【SystemVerilog】SystemVerilog与C语言的接口

第十二章

Verilog 使用编程语言接口(PLI)来跟 C 语言程序交互。PLI 先后经历了三代变化(TF、ACC 和 VPI 程序),使用 PLI 可以生成延迟计算器,以连接和同步多个仿真器,并增加诸如波形显示等调试工具。但是,PLI 最大的优点同时也成为了它最大的缺点------即使你只想通过 PLI 连接一个简单的 C 程序,也要写大量的代码,并理解很多概念,包括多个仿真阶段的同步、调用段、实例指针等等。此外,PLI 给仿真带来了额外的负担,因为为了保护 Verilog 的数据结构,仿真器必须不断地在 Verilog 和 C 语言域之间复制数据

SystemVerilog 引入了直接编程接口(DPI) ,它能更加简单地连接 C、C++ 或者其他非 Verilog 编程语言。一旦你声明或者使用 import 语句"导入"了一个 C 程序,你就可以像调用 SystemVerilog 中的子程序一样来调用它。此外,C 代码也可以调用 SystemVerilog 中的子程序

使用 DPI 可以很方便地连接 C 代码,这些 C 代码可以读取激励、包含一个参考模型或仅仅扩展 SystemVerilog 的功能

DPI vs PLI 对比
特性 PLI(编程语言接口) DPI(直接编程接口)
代码量 大量 少量
概念复杂度 高(同步、调用段、实例指针)
性能开销 高(数据复制)
易用性

传递简单的数值

如何在 SystemVerilog 和 C 语言之间传递整数类型

如何在 SystemVerilog 和 C 语言里定义子程序及其参数

传递整数和实数类型

SystemVerilog 和 C 之间可以传递的最基本的数据类型就是 int。它是一个双状态、32 位的数据类型

例 12.1 SystemVerilog 代码调用 C 语言子程序 factorial
复制代码
import "DPI-C" function int factorial(input int i);

program automatic test;
    initial begin
        for (int i = 1; i <= 10; i++)
            $display("%0d! = %0d", i, factorial(i));
    end
endprogram
例 12.2 C 语言 factorial 函数
复制代码
int factorial(int i) {
    if (i <= 1) return 1;
    else return i * factorial(i - 1);
}
语法 说明
import "DPI-C" 声明这是一个 DPI 子程序,使用其他语言(如 C/C++)实现
function int factorial SystemVerilog 中的函数原型
input int i 参数方向为 input,类型为 int

导入(import)声明

import 声明定义了 C 任务和函数的原型,但使用的是 SystemVerilog 的数据类型:

  • 带有返回值的 C 函数 → SystemVerilog 函数

  • void 类型的 C 函数 → SystemVerilog 任务或者 void 函数

例 12.3 改变导入函数的名字
复制代码
program automatic test;
    // 改变 C 函数名 "test" 为 "my_test"
    import "DPI-C" test = function void my_test();

    initial my_test();

    // C 函数与关键词同名,需要修改函数名
    import "DPI-C" \expect = function int fexpect();
    // ...
    if (actual != fexpect()) $display("ERROR");
    // ...
endprogram

命名冲突处理

场景 解决方案
C 函数名与 SystemVerilog 保留字冲突 使用 \expect 转义或重命名
C 函数名与已有符号冲突 使用 old_name = new_name 重命名

作用域规则

被导入的子程序将只在它被声明的空间中有效。如果你需要在代码的多个地方调用同一个导入函数,可以将 import 声明放在一个 package 中,并在需要的地方 import 该 package

参数方向

导入的 C 子程序可以有多个参数或者没有参数

方向 说明
input(缺省) 数据从 SystemVerilog 流向 C 函数
output 数据从 C 函数流向 SystemVerilog
inout 数据双向流动
ref ❌ 不支持
例 12.4 参数方向
复制代码
import "DPI-C" function int addmul(input int a, b, output int sum);
import "DPI-C" function void stop_model();
例 12.5 参数为常数的 factorial 子程序
复制代码
int factorial(const int i) {
    if (i <= 1) return 1;
    else return i * factorial(i - 1);
}

在 C 代码中将输入参数定义为 const,一旦对输入变量进行写操作,C 语言编译器就会报错,从而减少漏洞

参数类型

通过 DPI 传递的每个变量都有两个相匹配的定义,一个是 SystemVerilog 的,一个是 C 语言的。需要确保使用的是兼容的数据类型

表 12.1 SystemVerilog 和 C 语言之间的数据类型映射
SystemVerilog C(输入) C(输出)
byte char char*
shortint short int short int*
int int int*
longint long long int long int*
shortreal float float*
real double double*
string const char* char*
string[N] const char* char*
bit svBitunsigned char svBit*unsigned char*
logic, reg svLogicunsigned char svLogic*unsigned char*
bit[N:0] const svBitVecVal* svBitVecVal*
reg[N:0], logic[N:0] const svLogicVecVal* svLogicVecVal*
open array[] const svOpenArrayHandleconst void* svOpenArrayHandlevoid*

重要注意事项

要点 说明
映射不精确 bit 映射到 svBit,最终映射为 unsigned char
返回值限制 只能返回"小类型":voidbyteshortintintlongintrealshortrealstringbitlogic
不能返回向量 函数不能返回 bit[6:0] 这样的向量

导入数学库函数

例 12.6 导入 C 数学函数
复制代码
import "DPI-C" function real sin(input real r);
// ...
initial $display("sin(0) = %f", sin(0.0));

可以直接调用 C 语言数学函数库中的多个函数,而不需要使用 C 封装(wrapper),减少了需要编写的代码量


概念 说明
DPI 直接编程接口,比 PLI 更简单
import "DPI-C" 声明导入 C 函数
数据类型映射 使用表 12.1 确保类型匹配
参数方向 inputoutputinout(不支持 ref
命名冲突 使用 old=new 重命名
作用域 package 中统一管理 import
返回值限制 仅支持"小类型"

连接简单的C子程序

C 代码可能包含一个仿真模型,例如一个处理器,这个仿真模型跟 Verilog 模型一起被例化

C 代码也可能是一个跟 Verilog 事务级或者周期级的模型相对等的参考模型

以一个 C/C++ 描述的 7 位计数器为例,虽然该计数器非常简单,但是它具有一个复杂模型的所有构成,包括输入、输出、保存调用的内部数值的存储空间以及对多次例化的支持

使用静态变量的计数器

例 12.7 使用一个静态变量的计数器函数
复制代码
#include <svdpi.h>

void counter7(svBitVecVal * o,
              const svBitVecVal * i,
              const svBit reset,
              const svBit load) {
    static unsigned char count = 0;    // 静态的计数变量

    if (reset) count = 0;              // 复位
    else if (load) count = *i;         // 加载数值
    else count++;                      // 计数
    count &= 0x7f;                     // 最高位清 0

    *o = count;
}

数据类型映射

SystemVerilog C 语言 说明
bit svBitunsigned char 双状态比特
bit [6:0] svBitVecVal* 双状态向量
const 标记输入为只读 防止意外修改

头文件 svdpi.h 包含了 SystemVerilog DPI 结构和方法的定义

例 12.8 使用静态存储的 7 位计数器的测试平台
复制代码
import "DPI-C" function void counter7(output bit [6:0] out,
                                      input bit [6:0] in,
                                      input bit reset,
                                      input bit load);

program automatic counter;
    bit [6:0] out, in;
    bit reset, load;

    initial begin
        $monitor("SV: out=%3d, in=%3d, reset=%0d, load=%0d\n",
                 out, in, reset, load);
        reset = 0;
        load = 0;
        in = 126;
        out = 42;
        counter7(out, in, reset, load);    // 使用缺省值

        #10 reset = 1;
        counter7(out, in, reset, load);    // 复位
        // ...
    end
endprogram

chandle 数据类型

chandle 数据类型允许你在 SystemVerilog 代码中存储一个 C/C++ 指针。一个 chandle 变量的宽度足够在其被编译的机器上保存一个指针变量,例如 32 位或者 64 位

问题:例 12.7 中如果设计中仅存在一个实例,计数器就能很好地工作。但如果需要多次例化一个 C 程序,则不能在 C 代码中把变量保存在静态变量中

例 12.9 使用实例存储的计数器程序
复制代码
#include <svdpi.h>
#include <malloc.h>
#include <veriuser.h>

typedef struct {    // 保存计数值的结构
    unsigned char cnt;
} c7;

// 创建一个计数器结构
void* counter7_new() {
    c7* c = (c7*) malloc(sizeof(c7));
    c->cnt = 0;
    return c;
}

// 计数器运行一个周期
void counter7(c7* inst,
              svBitVecVal* count,
              const svBitVecVal* i,
              const svBit reset,
              const svBit load) {

    if (reset) inst->cnt = 0;          // 复位
    else if (load) inst->cnt = *i;     // 加载数值
    else inst->cnt++;                  // 计数
    inst->cnt &= 0x7f;                 // 最高位置 0

    *count = inst->cnt;                // 赋值给输出变量
    io_printf("C: count=%d, i=%d, reset=%d, load=%d\n",
              *count, *i, reset, load);
}
要点 说明
counter7_new 构建计数器实例,返回 chandle 指针
c7 结构 保存每个实例的独立计数值
io_printf PLI 任务,与 $display 写入同一输出(包含日志文件)
veriuser.h 包含 io_printf 的定义
例 12.10 使用独立实例存储空间的 7 位计数器的测试平台
复制代码
import "DPI-C" function chandle counter7_new();
import "DPI-C" function void counter7(input chandle inst,
                                      output bit [6:0] out,
                                      input bit [6:0] in,
                                      input bit reset,
                                      input bit load);

program automatic test;
    // 测试计数器的两个实例
    initial begin
        bit [6:0] o1, o2, i1, i2;
        bit reset, load, clk1;
        chandle inst1, inst2;    // 指向 C 中的存储空间

        inst1 = counter7_new();
        inst2 = counter7_new();

        fork
            forever #10 clk1 = ~clk1;
            forever @(posedge clk1) begin
                counter7(inst1, o1, i1, reset, load);
                counter7(inst2, o2, i2, reset, load);
            end
        join_none

        reset = 0;
        load = 0;
        i1 = 120;
        i2 = 10;

        @(negedge clk1);
        load = 1;

        @(negedge clk1);
        load = 0;
        // ...
    end
endprogram

值的压缩(packed)

字符串"DPI-C"表明你在使用压缩值的表示方式。这种方式将 SystemVerilog 变量保存在含有一个或者多个元素的 C 数组中

数据类型 C 语言类型
双状态变量 svBitVecVal
双状态数组 多个 svBitVecVal 元素
位宽转换宏
复制代码
SV_PACKED_DATA_NELEMS(40)    // 40 位 → 2 个 32 位字

40 比特双状态变量的存储(图 12.1)

复制代码
     字 0 (31:0)         字 1 (39:32)
┌─────────────────┬──────────┬──────────┐
│    bit31:0      │  bit39:32│  unused  │
└─────────────────┴──────────┴──────────┘
        32 位           8 位     24 位

注意:出于性能上的考虑,SystemVerilog 仿真器可能不会在调用了一个 DPI 函数之后屏蔽变量未使用的高位,所以 SystemVerilog 变量的值可能会产生错误。在 C 语言中需要确保这些变量的正确使用

四状态数值

SystemVerilog 中的所有四状态比特变量在仿真器中使用两个比特进行存储,通常称为 avalbval

表 12.2 四状态比特编码
四状态值 aval bval 四状态值 aval bval
0 0 0 Z 0 1
1 1 0 X 1 1
存储方式
SystemVerilog C 语言类型 说明
logic f(单比特) unsigned char aval 在 bit0,bval 在 bit1
logic [31:0] lword svLogicVecVal 一对 32 位(aval + bval)
logic [39:0] 多个 svLogicVecVal 第一个存低 32 位,第二个存高 8 位

40 比特四状态变量的存储(图 12.2)

例 12.11 检查 z 和 x 值的计数器测试平台
复制代码
import "DPI-C" function chandle counter7_new();
import "DPI-C" function void counter7(input chandle inst,
                                      output logic [6:0] out,
                                      input logic [6:0] in,
                                      input logic reset,
                                      input logic load);
例 12.12 检查 x 和 z 值的计数器程序
复制代码
void counter7(c7* inst,
              svLogicVecVal* count,
              const svLogicVecVal* i,
              const svLogic reset,
              const svLogic load) {

    // 检查标量的 bval 位(X/Z 检测)
    if (reset & 0x2) {
        io_printf("Error: Z or X detected on reset\n\n");
        return;
    }

    if (load & 0x2) {
        io_printf("Error: Z or X detected on load\n\n");
        return;
    }

    // 检查 7 比特向量的 bval 位
    if (i->bval) {
        io_printf("Error: Z or X detected on i\n\n");
        return;
    }

    if (reset) inst->cnt = 0;
    else if (load) inst->cnt = i->aval;
    else inst->cnt++;
    inst->cnt &= 0x7f;

    count->aval = inst->cnt;    // 赋值给输出变量
    count->bval = 0;            // 清除 bval
}
强制终止仿真
复制代码
#include <vpi_user.h>

// 在导入函数中强制终止仿真
vpi_control(vpiFinish, 0);

vpiFinish 表示仿真器在导入函数返回后执行 $finish 系统任务

从双状态数值转换到四状态数值

如果你的 DPI 应用程序使用的是双状态类型,但你想让它转变成能够处理四状态类型,可以按照以下步骤操作:

SystemVerilog 方面

修改前(双状态) 修改后(四状态)
bit logic
int integer

C 程序方面

修改前 修改后
svBitVecVal svLogicVecVal
直接访问值 使用 .aval 前缀
无需检查 X/Z 检查 bval 位是否存在 X/Z
直接写入 写入时清除 bval

概念 说明
svdpi.h DPI 必需的头文件
chandle 存储 C/C++ 指针
svBitVecVal 双状态向量类型
svLogicVecVal 四状态向量类型(含 aval/bval
io_printf C 代码中打印到仿真日志
SV_PACKED_DATA_NELEMS(N) 计算 N 位所需的 32 位字数

调用C++程序

在 SystemVerilog 中可以使用 DPI 调用 C 或者 C++ 子程序。模型的抽象层次不同,则调用 C++ 代码的方法也有所不同

C++ 中的计数器

例 12.13 计数器类
复制代码
class Counter7 {
public:
    Counter7();
    void counter7_signal(svBitVecVal* count,
                         const svBitVecVal* i,
                         const svBit reset,
                         const svBit load);
private:
    unsigned char cnt;
};

Counter7::Counter7() {
    cnt = 0;    // 计数器初始化
}

void Counter7::counter7_signal(svBitVecVal* count,
                               const svBitVecVal* i,
                               const svBit reset,
                               const svBit load) {
    if (reset) cnt = 0;              // 复位
    else if (load) cnt = *i;         // 加载数值
    else cnt++;                      // 计数
    cnt &= 0x7F;                     // 最高位清 0
    *count = cnt;
}

静态方法

关键限制:DPI 只能调用静态的 C 或者 C++ 方法,即在链接时已经存在的方法。SystemVerilog 代码不能调用对象中的 C++ 方法,因为被调用的对象在目标代码链接器运行时还不存在

解决方案 :创建可以与 C++ 动态对象和方法通信的静态方法(封装函数)

例 12.14 静态方法和链接
复制代码
// 创建计数器对象,返回句柄
extern "C" void* counter7_new() {
    return new Counter7;
}

// 调用一个计数器实例,并传递信号值
extern "C" void counter7(void* inst,
                         svBitVecVal* count,
                         const svBitVecVal* i,
                         const svBit reset,
                         const svBit load) {
    Counter7* c7 = (Counter7*) inst;
    c7->counter7_signal(count, i, reset, load);
}
extern "C" 的作用
作用 说明
使用 C 调用风格 告诉 C++ 编译器使用 C 的调用约定
禁止名字调整(name mangling) 确保函数名在链接时不变
两种用法 单独加在函数前,或放入 extern "C" { ... } 块中
复制代码
// 方式1:单独使用
extern "C" void* counter7_new() { ... }

// 方式2:块使用
extern "C" {
    void* counter7_new() { ... }
    void counter7(...) { ... }
}

从测试平台的角度来看,C++ 计数器看起来就像每一个实例都独立保存计数值的计数器,可以使用如例 12.10 所示的同一个测试平台

和事务级(Transaction Level)C++ 模型通信

前面给出的 C/C++ 代码都是跟 SystemVerilog 在信号级通信 的较低级别的模型。这样做的效率比较低------计数器在每一个时钟沿都会被调用,即使数据或者输入控制信号没有任何变化。当需要创建复杂设备的模型时(例如处理器和网络设备),使用事务级通信会使仿真速度加快

例 12.15 使用方法通信的 C++ 计数器
复制代码
class Counter7 {
public:
    Counter7();
    void count();
    void load(const svBitVecVal* i);
    void reset();
    int get();
private:
    unsigned char cnt;
};

Counter7::Counter7() {
    cnt = 0;    // 计数器初始化
}

void Counter7::count() {
    cnt = cnt + 1;
    cnt &= 0x7F;    // 最高位清 0
}

void Counter7::load(const svBitVecVal* i) {
    cnt = *i;
    cnt &= 0x7F;    // 最高位清 0
}

void Counter7::reset() {
    cnt = 0;
}

int Counter7::get() {
    return cnt;
}
例 12.16 C++ 事务级计数器的静态封装
复制代码
#ifdef __cplusplus
extern "C" {
#endif

void* counter7_new() {
    return new Counter7;
}

void counter7_count(void* inst) {
    Counter7* c7 = (Counter7*) inst;
    c7->count();
}

void counter7_load(void* inst, const svBitVecVal* i) {
    Counter7* c7 = (Counter7*) inst;
    c7->load(i);
}

void counter7_reset(void* inst) {
    Counter7* c7 = (Counter7*) inst;
    c7->reset();
}

int counter7_get(void* inst) {
    Counter7* c7 = (Counter7*) inst;
    return c7->get();
}

#ifdef __cplusplus
}
#endif
例 12.17 使用方法的 C++ 模型的测试平台
复制代码
import "DPI-C" function chandle counter7_new();
import "DPI-C" function void counter7_count(input chandle inst);
import "DPI-C" function void counter7_load(input chandle inst,
                                           input bit [6:0] i);
import "DPI-C" function void counter7_reset(input chandle inst);
import "DPI-C" function int counter7_get(input chandle inst);

// 用类封装计数器接口以隐藏 C++ 实例的句柄
class Counter7;
    chandle inst;

    function new();
        inst = counter7_new();
    endfunction

    function void count();
        counter7_count(inst);
    endfunction

    function void load(bit [6:0] val);
        counter7_load(inst, val);
    endfunction

    function void reset();
        counter7_reset(inst);
    endfunction

    function bit [6:0] get();
        return counter7_get(inst);
    endfunction
endclass : Counter7
例 12.18 使用方法的 C++ 模型的测试平台
复制代码
program automatic counter;
    Counter7 c1;

    initial begin
        c1 = new();

        c1.reset();
        $display("SV: Post reset: counter1=%0d", c1.get());

        c1.load(126);
        if (c1.get() != 126) $display("Error in load");

        c1.count();    // count = 127
        c1.count();    // count = 0
        if (c1.get() != 0) $display("Error in rollover");
    end
endprogram

信号级 vs 事务级模型对比

特性 信号级模型 事务级模型
通信方式 每个时钟沿传递信号 使用方法调用
仿真速度
抽象层次 低(RTL 级) 高(事务级)
适用场景 简单设备 处理器、网络设备
代码复杂度 简单 较复杂

信号级 vs 事务级 C++ 接口对比

复制代码
信号级 C++ 模型
┌─────────────────────────────────────────────────────────────┐
│  SystemVerilog                     C++                      │
│  ┌─────────────┐   每个时钟沿    ┌─────────────────────┐    │
│  │  测试平台   │ ──信号传递──▶   │  Counter7_signal()  │   │
│  └─────────────┘    (频繁调用)   └─────────────────────┘    │
└─────────────────────────────────────────────────────────────┘

事务级 C++ 模型
┌─────────────────────────────────────────────────────────────┐
│  SystemVerilog                     C++                      │
│  ┌─────────────┐   方法调用      ┌─────────────────────┐    │
│  │  测试平台   │ ──reset()───▶  │  reset()            │    │
│  │   (封装类)  │ ──load()───▶   │  load()             │    │
│  │             │ ──count()──▶   │  count()            │    │
│  └─────────────┘ ──get()────▶   │  get()              │    │
└─────────────────────────────────────────────────────────────┘

要点 说明
extern "C" 禁止 C++ 名字调整,使用 C 调用风格
静态方法封装 DPI 只能调用静态方法,通过封装访问动态对象
chandle 封装 在 SystemVerilog 类中包装 C++ 句柄
get() 返回 int 函数不能返回向量,只能返回"小类型"
事务级通信 使用方法调用代替信号传递,提高仿真速度

共享简单数组

一维数组------双状态

例 12.19 计算斐波那契级数的 C 子程序
复制代码
void fib(svBitVecVal data[20]) {
    int i;
    data[0] = 1;
    data[1] = 1;
    for (i = 2; i < 20; i++)
        data[i] = data[i-1] + data[i-2];
}
例 12.20 斐波那契子程序的测试平台
复制代码
import "DPI-C" function void fib(output bit [31:0] data[20]);

program automatic test;
    bit [31:0] data[20];

    initial begin
        fib(data);
        foreach (data[i]) $display(i,, data[i]);
    end
endprogram

注意 :斐波那契级数是在 SystemVerilog 中进行分配和存储的,它们是在 C 程序中计算出来的,没有任何办法可以在 SystemVerilog 中引用 C 分配的数组

一维数组------四状态

例 12.21 计算四状态输入数组的斐波那契 C 子程序
复制代码
void fib(svLogicVecVal data[20]) {
    int i;
    data[0].aval = 1;    // 赋值给 aval 和 bval
    data[0].bval = 0;
    data[1].aval = 1;
    data[1].bval = 0;
    for (i = 2; i < 20; i++) {
        data[i].aval = data[i-1].aval + data[i-2].aval;
        data[i].bval = 0;    // 别忘了将 bval 归零!
    }
}
例 12.22 带四状态数组的斐波那契 C 子程序的测试平台
复制代码
import "DPI-C" function void fib(output logic [31:0] data[20]);

program automatic test;
    logic [31:0] data[20];

    initial begin
        fib(data);
        foreach (data[i]) $display(i,, data[i]);
    end
endprogram

开放数组(open array)

当需要在 SystemVerilog 和 C 之间共享数组的时候,有两个选择:

方式 优点 缺点
直接映射 仿真速度最快 容易出错,数组大小变化需重新编写 C 代码
开放数组 通用性强,可操作任何大小数组 代码稍复杂

基本的开放数组

在 SystemVerilog 的 import 语句中使用空白的方括号 [] 来表明要传递的是一个开放数组

例 12.23 调用带有开放数组的 C 子程序的测试平台
复制代码
import "DPI-C" function void fib_oa(output bit [31:0] data[]);

program automatic test;
    bit [31:0] data[20];

    initial begin
        fib_oa(data);
        foreach (data[i])
            $display(i,, data[i]);
    end
endprogram
例 12.24 使用基本开放数组的 C 代码
复制代码
void fib_oa(const svOpenArrayHandle data_oa) {
    int i, *data;
    data = (int*) svGetArrayPtr(data_oa);
    data[0] = 1;
    data[1] = 1;
    for (i = 2; i <= 20; i++) {
        data[i] = data[i-1] + data[i-2];
    }
}

开放数组的方法

svdpi.h 中定义了很多可以访问开放数组的内容和范围的 DPI 方法。这些方法仅对定义为 svOpenArrayHandle 的开放数组句柄起作用,而不适用于 svBitVecVal 或者 svLogicVecVal 类型的指针

表 12.3 开放数组查询函数
函数 描述
int svLeft(h, d) 维数 d 的左边界
int svRight(h, d) 维数 d 的右边界
int svLow(h, d) 维数 d 的下界
int svHigh(h, d) 维数 d 的上界
int svIncrement(h, d) 如果左边界 >= 右边界则返回 1,否则返回 0
int svSize(h, d) 维数 d 的元素总数(svHigh - svLow + 1)
int svDimension(h) 开放数组的维数
int svSizeOfArray(h) 以字节计量的数组大小

变量 hsvOpenArrayHandle 类型,dint 类型

表 12.4 开放数组的定位函数
函数 返回指针类型
void* svGetArrayPtr(h) 整个数组的存储位置
void* svGetArrElemPtr(h, i1, ...) 数组中的一个元素
void* svGetArrElemPtr1(h, i1) 一维数组中的一个元素
void* svGetArrElemPtr2(h, i1, i2) 二维数组中的一个元素
void* svGetArrElemPtr3(h, i1, i2, i3) 三维数组中的一个元素

传递大小未定义的开放数组

在例 12.24 中,C 函数假定数组有 5 个元素,编号从 0 到 4。例 12.25 调用了一个参数为二维数组的函数。C 函数使用了 svLowsvHigh 方法来确定数组的范围

例 12.25 调用参数为多维开放数组的 C 函数的测试平台
复制代码
import "DPI-C" function void mydisplay(output int h[][][]);

program automatic test;
    int a[6:1][8:3];    // 注意此处范围由高到低

    initial begin
        foreach (a[i, j]) a[i][j] = i + j;
        mydisplay(a);
        foreach (a[i, j]) $display("v: a[%0d][%0d]=%0d", i, j, a[i][j]);
    end
endprogram
例 12.26 参数为多维开放数组的 C 代码
复制代码
void mydisplay(const svOpenArrayHandle h) {
    int i, j;
    int lo1 = svLow(h, 1);
    int hi1 = svHigh(h, 1);
    int lo2 = svLow(h, 2);
    int hi2 = svHigh(h, 2);

    for (i = lo1; i <= hi1; i++) {
        for (j = lo2; j <= hi2; j++) {
            int *a = (int*) svGetArrElemPtr2(h, i, j);
            io_printf("C: a[%d][%d]=%d\n", i, j, *a);
            *a = i * j;
        }
    }
}
索引方法对比
方法 6:1 返回值 1:6 返回值
svLeft(h, 1) 6 1
svRight(h, 1) 1 6
svLow(h, 1) 1 1
svHigh(h, 1) 6 6

DPI 中压缩(packed)的开放数组

在 DPI 中,一个开放数组被视为拥有一个压缩的维度和一个或多个非压缩的维度。你可以传递多维的压缩数组,只要该压缩数组的元素大小跟形式参数的元素大小相同即可

例 12.27 压缩开放数组的测试平台
复制代码
import "DPI-C" function void view_pack(input bit [63:0] b64[,]);

program automatic test;
    bit [1:0][0:3][6:-1] bpack[9:1];

    initial begin
        foreach (bpack[i]) bpack[i] = i;
        bpack[2] = 64'h12345678_90abcdef;

        $display("SV: bpack[2] = %h", bpack[2]);          // 64 位
        $display("SV: bpack[2][0] = %h", bpack[2][0]);    // 32 位
        $display("SV: bpack[2][0][0] = %h", bpack[2][0][0]); // 8 位

        view_pack(bpack);
    end
endprogram : test
例 12.28 使用压缩开放数组的 C 代码
复制代码
void view_pack(const svOpenArrayHandle h) {
    int i;

    for (i = svLow(h, 1); i <= svHigh(h, 1); i++)
        io_printf("C: b64[%d]=%llx\n",
                  i, *(long long int*) svGetArrElemPtr1(h, i));
}

注意 :C 代码按照 %llx 的格式输出一个 64 位数值,然后将结果从 svGetArrElemPtr1 返回的指针强制转换成了 long long int 类型

数组传递方式对比

方式 SystemVerilog 声明 C 语言声明 适用场景
固定数组 bit [31:0] data[20] svBitVecVal data[20] 固定大小,追求速度
开放数组 bit [31:0] data[] svOpenArrayHandle h 大小可变,通用性强
压缩开放数组 bit [63:0] b64[,] svOpenArrayHandle h 多维压缩数组

要点 说明
svOpenArrayHandle 开放数组的句柄类型
svGetArrayPtr 获取整个数组的存储指针
svGetArrElemPtrN 获取 N 维数组元素的指针
svLow/svHigh 获取数组维度的上下界
压缩开放数组 多维压缩数组作为开放数组传递

共享复合类型

如何在 SystemVerilog 和 C 之间传递对象?

就类属性的内存映射方式来讲,这两种语言并不完全一致,所以不能直接共享对象。为了达到共享的目的,必须在两边创建相似的结构,并且使用压缩和解压缩方法对这两种格式进行转换

在 SystemVerilog 和 C 之间传递结构

下面是一个共享用于表示像素的简单结构变量的例子,该结构由三个字节组成,被压缩成一个字

例 12.29 共享结构的 C 代码
复制代码
typedef struct {
    unsigned char b, g, r;    // x86 小尾序
    // unsigned char r, g, b;  // SPARC 格式
} *p_rgb;

void invert(p_rgb rgb) {
    rgb->r = ~rgb->r;    // 色彩值取反
    rgb->g = ~rgb->g;
    rgb->b = ~rgb->b;
    io_printf("C: Invert rgb=%02x,%02x,%02x\n",
              rgb->r, rgb->g, rgb->b);
}
字节顺序注意事项
架构 字节存储顺序
Intel x86(小尾序) b, g, r(低权重字节在低地址)
Sun SPARC(大尾序) r, g, b(与 SystemVerilog 相同)

注意 :C 将 char 视为有符号变量,这可能会带来不可预测的结果,所以该结构对 char 类型加上 unsigned 限制

例 12.30 共享结构的测试平台
复制代码
typedef struct packed { bit [7:0] r, g, b; } RGB_T;

import "DPI-C" function void invert(inout RGB_T pstruct);

program automatic test;

class RGB;
    rand bit [7:0] r, g, b;

    function void display(string prefix = "");
        $display("%sRGB=%x,%x,%x", prefix, r, g, b);
    endfunction : display

    // 将类成员压缩到一个结构中
    function RGB_T pack();
        pack.r = r;
        pack.g = g;
        pack.b = b;
    endfunction : pack

    // 将结构解压后赋值给类成员
    function void unpack(RGB_T pstruct);
        r = pstruct.r;
        g = pstruct.g;
        b = pstruct.b;
    endfunction : unpack
endclass : RGB

initial begin
    RGB pixel;
    RGB_T pstruct;

    pixel = new();
    repeat (5) begin
        assert(pixel.randomize());        // 创建随机像素
        pixel.display("\nSV: before ");   // 打印像素值
        pstruct = pixel.pack();           // 转换为结构
        invert(pstruct);                  // 调用 C 函数将位取反
        pixel.unpack(pstruct);            // 把结构解压后赋值给类
        pixel.display("SV: after ");      // 打印
    end
end
endprogram
结构传递流程图
复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                    SystemVerilog 与 C 结构传递                      │
│                                                                     │
│  SystemVerilog 类                   C 结构                          │
│  ┌─────────────────┐              ┌─────────────────┐              │
│  │  pixel          │    pack()    │   RGB_T         │              │
│  │  r = 0xFF       │ ──────────▶ │   r = 0xFF      │              │
│  │  g = 0x80       │              │   g = 0x80      │              │
│  │  b = 0x00       │              │   b = 0x00      │              │
│  └─────────────────┘              └─────────────────┘              │
│         │                                  │                       │
│         │                                  ▼                       │
│         │                         ┌─────────────────┐              │
│         │                         │   C 结构        │              │
│         │                         │   r = ~r        │              │
│         │                         │   g = ~g        │              │
│         │                         │   b = ~b        │              │
│         │                         └─────────────────┘              │
│         │                                  │                       │
│         ▼                                  ▼                       │
│  ┌─────────────────┐    unpack()   ┌─────────────────┐              │
│  │  pixel          │ ◀─────────── │   RGB_T         │              │
│  │  r = ~0xFF      │               │   r = ~0xFF     │              │
│  │  g = ~0x80      │               │   g = ~0x80     │              │
│  │  b = ~0x00      │               │   b = ~0x00     │              │
│  └─────────────────┘               └─────────────────┘              │
└─────────────────────────────────────────────────────────────────────┘
压缩 vs 非压缩结构
结构类型 存储方式 说明
struct packed 连续存储 字节连续排列,节省空间
struct(非压缩) 每个成员独立存储 每个 8 位值保存成一个单字

在 SystemVerilog 和 C 之间传递字符串

使用 DPI 可以将字符串从 C 中回传给 SystemVerilog。你可能需要为结构的符号值传递一个字符串,或者为调试 C 代码而去获取一个能够表征代码内部状态的字符串

方法一:返回静态字符串指针(有风险)
复制代码
// 例 12.31 从 C 中返回一个字符串(使用静态变量)
char* print(p_rgb rgb) {
    static char s[12];
    sprintf(s, "%02x,%02x,%02x", rgb->r, rgb->g, rgb->b);
    return s;
}

风险 :多个函数并发调用时会引起内存共享问题。除非 SystemVerilog 编译器复制了输出字符串,否则后面的 print() 调用可能会覆盖前面调用的结果

注意:对导入函数的调用无法被 SystemVerilog 调度器中断

方法二:使用堆分配字符串(安全)
复制代码
// 例 12.32 从一个 C 的堆中返回一个字符串
#define PRINT_SIZE 12
#define MAX_CALLS 16
#define HEAP_SIZE PRINT_SIZE * MAX_CALLS

char* print(p_rgb rgb) {
    static char print_heap[HEAP_SIZE + PRINT_SIZE];
    char* s;
    static int heap_idx = 0;
    int nchars;

    s = &print_heap[heap_idx];
    nchars = sprintf(s, "%02x,%02x,%02x",
                     rgb->r, rgb->g, rgb->b);
    heap_idx += nchars + 1;    // 不要忘了 null 值!
    if (heap_idx > HEAP_SIZE)
        heap_idx = 0;
    return s;
}
字符串传递方式对比
方式 优点 缺点
静态字符串 实现简单 多调用并发时可能被覆盖
堆分配 支持并发调用 实现稍复杂

复合类型传递总结

类型 传递方式 关键要点
结构体 打包/解包 使用 packed 结构保持连续存储
字符串 返回指针 使用静态或堆分配存储
❌ 不能直接传递 需要通过 pack/unpack 转换为结构

要点 说明
不能直接共享对象 SystemVerilog 和 C++ 类内存映射不一致
压缩结构 使用 struct packed 确保连续存储
字节顺序 注意 x86(小尾序)和 SPARC(大尾序)的区别
pack/unpack 在类成员和结构之间转换
字符串传递 使用静态或堆分配的字符数组
unsigned char 避免 char 有符号导致的问题

纯导入方法和关联导入方法

导入方法分为纯(pure)导入、关联(context)导入和通用(generic)导入三种类型

三种导入方法对比
类型 关键字 特点 编译器优化
纯函数(pure) pure 仅依赖输入,无外部交互 ✅ 可优化
关联函数(context) context 需要上下文信息,可调用 PLI ❌ 有额外开销
通用函数(generic) 缺省 不调用 PLI,不使用全局变量 取决于实现
纯函数(Pure Function)

一个纯函数将严格根据其输入来计算输出,跟外部环境没有任何其他交互:

  • 不会访问任何全局或者静态变量

  • 不会进行文件操作

  • 不会跟函数体以外的事务(如操作系统、进程、共享内存和套接字)有交互

例 12.33 导入一个纯函数
复制代码
import "DPI-C" pure function int factorial(input int i);
import "DPI-C" pure function real sin(input real in);

编译器优化

  • 如果没有使用纯函数的输出,编译器会优化掉对该函数的调用

  • 对于输入参数相同的两次调用,编译器会将第二次调用直接用第一次的输出结果替换

例 12.34 导入关联任务
复制代码
import "DPI-C" context task call_sv(bit [31:0] data);

重要:关联导入方法需要记录调用的上下文环境,这会给仿真器带来额外的开销。所以除非确实需要,否则不要将方法定义为关联方法。另一方面,如果一个通用导入方法调用了一个导出任务或者一个访问 SystemVerilog 数据对象的 PLI 函数,会导致仿真器崩溃

在C中与SystemVerilog通信

前面的例子示范了如何在 SystemVerilog 模型中调用 C 代码。DPI 也允许在 C 代码中调用 SystemVerilog 中的方法。被调用的 SystemVerilog 方法可以是一个保存 C 函数操作结果的简单任务,或者是一个表征部分硬件模型的耗时任务

一个简单的导出方法

例 12.35 导出一个 SystemVerilog 函数
复制代码
module block;
    import "DPI-C" context function void c_display();
    export "DPI-C" function sv_display;    // 没有类型定义或者参数

    initial c_display();

    function void sv_display();
        $display("SV: block");
    endfunction
endmodule : block

重要export 声明看起来"光秃秃的",因为语言参考手册禁止带任何返回值声明或者参数,甚至不能加上函数声明通常使用的空括号。这些信息已经在函数声明时给出了

例 12.36 在 C 中调用一个 SystemVerilog 导出函数
复制代码
extern void sv_display();

void c_display() {
    io_printf("C: c_display\n");
    sv_display();
}
例 12.37 简单导出函数的执行结果
复制代码
C: c_display
SV: block

调用 SystemVerilog 函数的 C 函数

例 12.38 简单内存模型的 SystemVerilog 模块
复制代码
module memory;
    import "DPI-C" function read_file(string fname);
    export "DPI-C" function mem_build;    // 没有类型定义或者参数

    initial
        read_file("mem.dat");

    int mem[];

    function mem_build(input int size);
        mem = new[size];    // 分配动态内存元素
    endfunction
endmodule : memory
例 12.39 读取简单命令文件并调用导出函数的 C 代码
复制代码
extern void mem_build(int);

int read_file(char* fname) {
    int cmd;
    FILE* file;

    file = fopen(fname, "r");
    while (!feof(file)) {
        cmd = fgetc(file);
        switch (cmd) {
            case 'M': {
                int hi;
                fscanf(file, "%d %d", &hi);
                mem_build(hi);
                break;
            }
        }
    }
    fclose(file);
}
例 12.40 简单内存模型的命令文件
复制代码
M 100

调用 SystemVerilog 任务的 C 任务

一个真实的内存模型会含有诸如读写之类消耗时间的操作,所以必须使用任务来建模

例 12.41 带有导出任务的 SystemVerilog 内存模型模块
复制代码
module memory;
    import "DPI-C" context task read_file(string fname);
    export "DPI-C" task mem_read;
    export "DPI-C" task mem_write;
    export "DPI-C" function mem_build;

    initial read_file("mem.dat");

    int mem[];

    function mem_build(input int size);
        mem = new[size];
    endfunction

    task mem_read(input int addr, output int data);
        #20 data = mem[addr];
    endtask

    task mem_write(input int addr, input int data);
        #10 mem[addr] = data;
    endtask
endmodule : memory
例 12.43 命令文件
复制代码
M 100
W 12 34
W 99 8
R 12 34

调用对象中的方法

可以导出 SystemVerilog 方法,但是定义在类中的方法除外。这个限制类似于对导入静态 C 函数的限制,因为当 SystemVerilog 编译器转译代码时,对象还不存在

解决方案 :在 SystemVerilog 和 C 代码之间传递一个对象引用。SystemVerilog 句柄不能通过 DPI 直接传递,但可以定义一个句柄数组,然后在两种语言之间传递数组的索引

例 12.45 带有内存模型类的 SystemVerilog 模块
复制代码
module memory;
    import "DPI-C" context task read_file(string fname);
    export "DPI-C" task mem_read;
    export "DPI-C" task mem_write;
    export "DPI-C" function mem_build;

    initial read_file("mem.dat");    // 调用 C 代码读取文件

    class Memory;
        int mem[];
        function new(input int size);
            mem = new[size];
        endfunction

        task mem_read(input int addr, output int data);
            #20 data = mem[addr];
        endtask

        task mem_write(input int addr, input int data);
            #10 mem[addr] = data;
        endtask : mem_write
    endclass : Memory

    Memory memq[$];    // 内存对象队列

    // 创建一个新的内存实例并将其压入队列
    function int mem_build(input int size);
        Memory m;
        m = new(size);
        memq.push_back(m);
        return memq.size() - 1;
    endfunction

    task mem_read(input int idx, addr, output int data);
        memq[idx].mem_read(addr, data);
    endtask

    task mem_write(input int idx, addr, input int data);
        memq[idx].mem_write(addr, data);
    endtask
endmodule : memory
例 12.44 带有导出方法的 OOP 内存模型的命令文件
复制代码
M0 1000
M1 2000
W0 12 34
W1 12 88
W0 99 18
R1 22 44
R0 12 34
R1 12 88

上下文(context)的含义

导入函数的上下文是该函数定义所在的位置,比如 $unit、模块、program 或者 package 作用域,这一点跟普通的 SystemVerilog 方法是一样的。如果你把一个函数导入到两个不同的作用域,对应的 C 代码会依据 import 语句所在位置的上下文执行

例 12.47 简单导出例子的第二个模块
复制代码
module top;
    import "DPI-C" context function void c_display();
    export "DPI-C" function sv_display;

    block bl();
    initial c_display();

    function void sv_display();
        $display("SV: top");
    endfunction
endmodule : top

module block;
    import "DPI-C" context function void c_display();
    export "DPI-C" function sv_display;

    initial c_display();

    function void sv_display();
        $display("SV: block");
    endfunction
endmodule : block
例 12.48 含有两个模块的简单例子的输出
复制代码
C: c_display
SV: block
C: c_display
SV: top

设置导入函数的作用域

如同 SystemVerilog 代码可以在局部作用域调用方法,导入的 C 方法也可以在它默认的上下文之外调用方法。使用 svGetScope 方法可以获得当前作用域的句柄,然后就可以在对 svSetScope 的调用中使用该句柄,使得 C 代码认为它处在另外一个上下文中

例 12.49 获取和设置上下文的 C 代码
复制代码
extern void sv_display();
svScope my_scope;

void save_my_scope() {
    my_scope = svGetScope();
}

void c_display() {
    // 打印当前作用域
    io_printf("\nC: c_display called from scope %s\n",
              svGetNameFromScope(svGetScope()));
    // 设置新的作用域
    svSetScope(my_scope);
    io_printf("C: calling %s.sv_display\n",
              svGetNameFromScope(svGetScope()));
    sv_display();
}

作用域相关函数

函数 描述
svGetScope() 获取当前作用域的句柄
svSetScope(scope) 设置当前作用域
svGetNameFromScope(scope) 获取作用域的名称字符串
svGetScopeFromName(name) 通过名称获取作用域句柄
例 12.50 调用获取和设置上下文方法的模块
复制代码
module block;
    import "DPI-C" context function void c_display();
    import "DPI-C" context function void save_my_scope();
    export "DPI-C" function sv_display;

    function void sv_display();
        $display("SV: %m");
    endfunction : sv_display

    initial begin
        save_my_scope();
        c_display();
    end
endmodule : block

module top;
    import "DPI-C" context function void c_display();
    export "DPI-C" function sv_display;

    function void sv_display();
        $display("SV: %m");
    endfunction : sv_display

    block b1();
    // ...
    initial #1 c_display();
endmodule : top
例 12.51 svSetScope 代码的输出
复制代码
C: c_display called from top.b1
C: Calling top.b1.sv_display
SV: top.b1.sv_display

C: c_display called from top
C: Calling top.b1.sv_display
SV: top.b1.sv_display

完整 DPI 架构图

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                         SystemVerilog 域                            │
│                                                                     │
│  ┌─────────────────────────────────────────────────────────────┐    │
│  │  测试平台 / 模块                                            │    │
│  │  ┌─────────────────────────────────────────────────────┐   │     │
│  │  │  import "DPI-C" function ...                        │   │     │
│  │  │  export "DPI-C" function sv_display                 │   │     │
│  │  │  export "DPI-C" task mem_read                       │   │     │
│  │  └─────────────────────────────────────────────────────┘   │     │
│  └─────────────────────────────────────────────────────────────┘    │
│                              │                                      │
│                              │ DPI 边界                             │
│                              ▼                                      │
└─────────────────────────────────────────────────────────────────────┘
                               │
                               ▼
┌─────────────────────────────────────────────────────────────────────┐
│                              C 域                                   │
│                                                                     │
│  ┌─────────────────────────────────────────────────────────────┐    │
│  │  C 代码                                                    │     │
│  │  ┌─────────────────────────────────────────────────────┐   │     │
│  │  │  void c_display() { sv_display(); }                 │   │     │
│  │  │  void mem_build(int size) { ... }                   │   │     │
│  │  │  void save_my_scope() { my_scope = svGetScope(); }  │   │     │
│  │  └─────────────────────────────────────────────────────┘   │     │
│  └─────────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────────┘

概念 说明
pure 纯函数,仅依赖输入,可优化
context 关联函数,需要上下文信息,有额外开销
export 导出 SystemVerilog 方法供 C 调用
导出限制 不能导出类中的方法
句柄传递 通过句柄数组索引传递对象引用
svGetScope 获取当前上下文
svSetScope 设置当前上下文

与其他语言交互

最简单的办法是调用 Verilog 的 $system() 任务。如果需要命令的返回值,使用 Unix 的 system() 函数和 WEXITSTATUS 宏定义

例 12.52 调用封装 Perl 代码的 C 函数的 SystemVerilog 代码
复制代码
import "DPI-C" function int call_perl(string s);

program automatic perl_test;
    int ret_val;
    string script;

    initial begin
        if (!$test$plusargs("script")) begin
            $display("No +script switch found");
            $finish;
        end
        $value$plusargs("script=%s", script);
        $display("SV: Running '%0s'", script);
        ret_val = call_perl(script);
        $display("SV: Perl script returned %0d", ret_val);
    end
endprogram : perl_test
例 12.53 Perl 脚本的 C 封装
复制代码
#include "vc_hdrs.h"
#include <stdlib.h>
#include <wait.h>

int call_perl(const char* command) {
    int result = system(command);
    return WEXITSTATUS(result);
}
例 12.54 C 和 SystemVerilog 调用的 Perl 脚本
复制代码
#!/usr/local/bin/perl
print "Perl: Hello world!\n";
exit(3);
执行流程
复制代码
┌─────────────────────────────────────────────────────────────────────┐
│  SystemVerilog                     C                      Perl     │
│  ┌─────────────────┐     ┌─────────────────┐    ┌───────────────┐  │
│  │ call_perl(      │────▶│ system()       │───▶│ Perl 脚本     │  │
│  │   "script.pl")  │     │ WEXITSTATUS()   │    │ print & exit  │   │
│  └─────────────────┘     └─────────────────┘    └───────────────┘   │
│         │                         │                        │        │
│         │ 3 (返回值)              │ 3 (返回值)             │        │
│         │◀───────────────────────│◀──────────────────────│        │
└─────────────────────────────────────────────────────────────────────┘
与其他语言交互的方式
方式 说明 适用场景
$system() 直接调用系统命令 简单命令执行
DPI + system() 通过 C 封装获取返回值 需要命令返回值
DPI 通用接口 任何支持 C 接口的语言 Python、Perl、Tcl 等

DPI 使得你能够像调用 SystemVerilog 子程序一样调用 C 子程序,并将 SystemVerilog 类型变量直接传递给 C

DPI vs PLI 对比
特性 PLI DPI
代码量 大(每个系统任务需要 4+ 个 C 子程序)
复杂度 高(需要创建参数列表、管理上下文)
性能开销
双向调用 复杂 简单(C ↔ SystemVerilog)
数据类型映射 复杂 直接
学习曲线 陡峭 平缓
DPI 的主要优势
优势 说明
简单调用 像调用 SystemVerilog 子程序一样调用 C
直接传递 SystemVerilog 类型变量直接传递给 C
双向通信 C 代码可以调用 SystemVerilog 子程序
外部控制 允许外部应用程序控制仿真过程
低开销 比 PLI 开销更小
使用 DPI 的最大挑战

使用 DPI 的最大难点在于将 SystemVerilog 的数据类型映射到 C,尤其是当你在两种语言之间共享结构和类的时候。如果你知道如何解决这个问题,那么几乎可以将任何的应用程序都连接到 SystemVerilog 上


主题 核心内容
12.1 传递简单的数值 intrealbitlogic 的映射
12.2 连接简单的 C 子程序 静态变量、chandle、四状态类型
12.3 调用 C++ 程序 extern "C"、封装方法、事务级模型
12.4 共享简单数组 双状态/四状态数组的传递
12.5 开放数组 svOpenArrayHandle、多维数组
12.6 共享复合类型 结构传递、字符串传递
12.7 纯导入/关联导入 purecontext 关键字
12.8 C 中与 SV 通信 export、导出任务、作用域管理
12.9 与其他语言交互 Perl 脚本调用

DPI 快速参考

概念 语法/类型 说明
导入函数 import "DPI-C" function ... 从 C 导入函数
导出函数 export "DPI-C" function ... 导出 SV 函数给 C
纯函数 pure 仅依赖输入,可优化
关联函数 context 需要上下文信息
开放数组 svOpenArrayHandle 大小可变的数组
C 指针 chandle 存储 C/C++ 指针
双状态向量 svBitVecVal 32 位双状态向量
四状态向量 svLogicVecVal 32 位四状态向量(aval/bval)
作用域管理 svGetScope/svSetScope 管理调用上下文
相关推荐
W是笔名2 小时前
python___容器类型的数据___序列
开发语言·python
☆cwlulu2 小时前
try-throw-catch异常捕获流程
开发语言·c++
漂亮的摩托2 小时前
深感一无所长,准备试着从零开始写个富文本编辑器
开发语言·php
要开心吖ZSH2 小时前
Java事务与MySQL事务的关系及MVCC通俗解析
java·开发语言·mysql·mvcc
寻道码路2 小时前
LangChain4j Java AI 应用开发实战(二十六):多模型集成策略 —— OpenAI、DeepSeek、阿里百炼混合使用
java·开发语言·人工智能·ai
面朝大海,春不暖,花不开2 小时前
BPF与eBPF简介:核心概念与观测工具概览
开发语言·php·ebpf·bpf·性能观测
ch.ju2 小时前
Java Programming Chapter 4——Static code block
java·开发语言
弹简特2 小时前
【Java项目-企悦抽】04-项目演示+项目源码+AI赋能整理接口文档
java·开发语言
郝学胜-神的一滴2 小时前
Qt 高级编程 034:深耕QWidget底层内核—彻底吃透无边框窗口设计核心原理
开发语言·c++·qt·程序人生·软件开发·用户界面