はじめに
SystemVerilogにはCの関数を呼び出せるDPI(Direct Programming Interface)という機能があります。
Cが呼べるなら当然Rustも呼べる、というわけでやってみました。
リポジトリはこちらになります。
準備
まずRustプロジェクトの準備です。
CのAPIを提供する共有ライブラリを作るので
$ cargo new --lib dpi
としてCargo.tomlにクレートタイプを追記します。
[lib]
crate-type = ["cdylib"]
あとはSystemVerilogから呼びたい関数をsrc/lib.rs
に書いていけばOKです。
最初のサンプル
SystemVerilog側で呼びたい関数のプロトタイプを定義します。
import "DPI-C" function byte add_i8 (input byte x, input byte y);
続いてRust側です。SystemVerilogのbyte
型は符号付き8ビットなので、Rustではi8
です。
またCのABIが期待されるので#[no_mangle]
とextern "C"
が必要です。
#[no_mangle]
pub extern "C" fn add_i8(x: i8, y: i8) -> i8 {
x + y
}
色々な組み込み型
同様に64ビットまでの数値型はOKです。
import "DPI-C" function shortint add_i16 (input shortint x, input shortint y);
import "DPI-C" function int add_i32 (input int x, input int y);
import "DPI-C" function longint add_i64 (input longint x, input longint y);
#[no_mangle]
pub extern "C" fn add_i16(x: i16, y: i16) -> i16 {
x + y
}
#[no_mangle]
pub extern "C" fn add_i32(x: i32, y: i32) -> i32 {
x + y
}
#[no_mangle]
pub extern "C" fn add_i64(x: i64, y: i64) -> i64 {
x + y
}
参照渡し
戻り値ではなく、参照を渡して書き換えたい場合です。SystemVerilog側はoutput
を使います。
import "DPI-C" function void add_by_output (input longint x, input longint y, output longint z);
Rust側は&mut
です。
#[no_mangle]
pub extern "C" fn add_by_output(x: i64, y: i64, z: &mut i64) {
*z = x + y
}
packed bit
SystemVerilog側でビット幅を指定する場合です。
import "DPI-C" function void xor_logic (input bit [511:0] x, input bit [511:0] y, output bit [511:0] z);
Rustからは&[u8;N]
に見えるようです。
#[no_mangle]
pub extern "C" fn xor_logic(x: &[u8; 64], y: &[u8; 64], z: &mut [u8; 64]) {
for i in 0..64 {
z[i] = x[i] ^ y[i];
}
}
unpacked array
配列を渡す場合です。
import "DPI-C" function void unpacked_array (input int x[10]);
Rustからは&[T;N]
に見えるようです。
#[no_mangle]
pub extern "C" fn unpacked_array(x: &[i32; 10]) {
print!("unpacked_array: ");
for i in 0..10 {
print!("{}", x[i]);
}
println!("");
}
文字列
SystemVerilogには組み込みのstring
型があります。
import "DPI-C" function void print (input string x);
Rustから見ると*const c_char
で来るのでCStr
に変換できます。
#[no_mangle]
pub extern "C" fn print(a: *const c_char) {
let a = unsafe { CStr::from_ptr(a) };
dbg!(a);
}
Rustのオブジェクト
Rustのstruct
などを返したい場合、SystemVerilog側ではchandle
です。
import "DPI-C" function chandle create();
Rust側は生ポインタです。
スタックに積んだオブジェクトを渡すことはできないのでBox
でヒープに確保して、into_raw
で所有権を放棄しつつ生ポインタにします。
所有権を放棄しないと関数を抜けたところで寿命が尽きてSystemVerilog側には無効なポインタが返ることになってしまいます。
#[derive(Debug)]
pub struct Object {
x: usize,
y: String,
}
#[no_mangle]
pub extern "C" fn create() -> NonNull<Object> {
let obj = Box::new(Object {
x: 10000,
y: String::from("Hello"),
});
let obj = Box::<Object>::into_raw(obj);
NonNull::new(obj).unwrap()
}
別途メモリ解放用の関数も必要です。from_raw
で所有権を取り戻せば、スコープアウトしたタイミングで解放されます。
import "DPI-C" function void destroy(input chandle x);
#[no_mangle]
pub extern "C" fn destroy(ptr: NonNull<Object>) {
let obj = unsafe { Box::<Object>::from_raw(ptr.as_ptr()) };
println!("{:?} is destroied", obj);
}
動作を確認
フリーのRTLシミュレータであるVerilatorでDPIが使えます。
以下のように一通り関数を呼んでみます。
module test;
chandle p;
int array[10] = '{0,1,2,3,4,5,6,7,8,9};
longint z1;
bit [511:0] z2;
initial begin
// use integral types
$display("%x + %x = %x", 1, 2, add_i8 (1,2));
$display("%x + %x = %x", 1, 2, add_i16 (1,2));
$display("%x + %x = %x", 1, 2, add_i32 (1,2));
$display("%x + %x = %x", 1, 2, add_i64 (1,2));
// use output
add_by_output(1,2,z1);
$display("%x + %x = %x", 1, 2, z1);
// use packed bit
xor_logic('h11,'h22,z2);
$display("%x ^ %x = %x", 'h11, 'h22, z2);
// Verilator can't compile
`ifndef VERILATOR
unpacked_array(array);
`endif
// use string
print("Hello World");
// use chandle
p = create();
destroy(p);
$finish;
end
endmodule
Githubにあるプロジェクトディレクトリでmake
を実行すればVerilatorで実行してみることができます。
結果は以下の通りです。
[src/lib.rs:49] a = "Hello World"
Object { x: 10000, y: "Hello" } is destroied
00000001 + 00000002 = 03
00000001 + 00000002 = 0003
00000001 + 00000002 = 00000003
00000001 + 00000002 = 0000000000000003
00000001 + 00000002 = 0000000000000003
00000011 ^ 00000022 = 942ed14d30202b2000000003000000030000004000000000adaead00adaeadc00000000000000000000000000000000000000000000000000000000000000033
- test.sv:43: Verilog $finish
だいたい問題ないですが、unpacked array引数は未対応なのかエラーになります。
またpacked bit
のoutput
で上位半分のビットが化けるようです。
上の例だとbit[511:0]
の上位256ビットに変な値が入っています。
商用のシミュレータでは問題なかったのでVerilatorのバグかもしれません。
packed bitの結果(修正版)
packed bitの結果がおかしいのはoutput
の型指定が抜けているからでした。
正しくbit
を指定すると結果も正しくなりました。
import "DPI-C" function void xor_logic (input bit [511:0] x, input bit [511:0] y, output bit [511:0] z); //OK
import "DPI-C" function void xor_logic (input bit [511:0] x, input bit [511:0] y, output [511:0] z); //NG
[src/lib.rs:52] a = "Hello World"
Object { x: 10000, y: "Hello" } is destroied
00000001 + 00000002 = 03
00000001 + 00000002 = 0003
00000001 + 00000002 = 00000003
00000001 + 00000002 = 0000000000000003
00000001 + 00000002 = 0000000000000003
00000011 ^ 00000022 = 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000033
- test.sv:45: Verilog $finish
型指定がない場合、暗黙の型としてlogic
が推定されますがこれは01xz
の4値型なので
Rust側からみると配列のサイズは倍の&mut [u8; 128]
となります。
このうち前半の64要素分しか書いていないので残り64要素分がメモリ初期値のランダムな値になったようです。
Verilatorの古いバージョンや商用シミュレータの場合は0クリアしてから渡してくるようで、
たまたま動いているように見えただけでした。