LoginSignup
16
8

More than 3 years have passed since last update.

SystemVerilogのDPIでRustを使ってみた

Last updated at Posted at 2020-01-30

はじめに

SystemVerilogにはCの関数を呼び出せるDPI(Direct Programming Interface)という機能があります。
Cが呼べるなら当然Rustも呼べる、というわけでやってみました。
リポジトリはこちらになります。

準備

まずRustプロジェクトの準備です。
CのAPIを提供する共有ライブラリを作るので

$ cargo new --lib dpi

としてCargo.tomlにクレートタイプを追記します。

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 bitoutputで上位半分のビットが化けるようです。
上の例だと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クリアしてから渡してくるようで、
たまたま動いているように見えただけでした。

16
8
8

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
8