SystemVerilogのDPIでRustを使ってみた

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

https://github.com/dalance/rust-dpi-sample

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

```console
$ cargo new --lib dpi
```

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

```toml:Cargo.toml
[lib]
crate-type = ["cdylib"]
```

あとはSystemVerilogから呼びたい関数を`src/lib.rs`に書いていけばOKです。

# 最初のサンプル

SystemVerilog側で呼びたい関数のプロトタイプを定義します。

```verilog
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"`が必要です。

```rust
#[no_mangle]
pub extern "C" fn add_i8(x: i8, y: i8) -> i8 {
    x + y
}
```

# 色々な組み込み型
同様に64ビットまでの数値型はOKです。

```verilog
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);
```

```rust
#[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`を使います。

```verilog
import "DPI-C" function void add_by_output (input longint x, input longint y, output longint z);
```

Rust側は`&mut`です。

```rust
#[no_mangle]
pub extern "C" fn add_by_output(x: i64, y: i64, z: &mut i64) {
    *z = x + y
}
```

# packed bit
SystemVerilog側でビット幅を指定する場合です。

```verilog
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]`に見えるようです。

```rust
#[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
配列を渡す場合です。

```verilog
import "DPI-C" function void unpacked_array (input int x[10]);
```

Rustからは`&[T;N]`に見えるようです。

```rust
#[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`型があります。

```verilog
import "DPI-C" function void print (input string x);
```

Rustから見ると`*const c_char`で来るので`CStr`に変換できます。

```rust
#[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`です。

```verilog
import "DPI-C" function chandle create();
```

Rust側は生ポインタです。
スタックに積んだオブジェクトを渡すことはできないので`Box`でヒープに確保して、`into_raw`で所有権を放棄しつつ生ポインタにします。
所有権を放棄しないと関数を抜けたところで寿命が尽きてSystemVerilog側には無効なポインタが返ることになってしまいます。

```rust
#[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`で所有権を取り戻せば、スコープアウトしたタイミングで解放されます。

```verilog
import "DPI-C" function void destroy(input chandle x);
```

```rust
#[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](https://www.veripool.org/wiki/verilator)でDPIが使えます。
以下のように一通り関数を呼んでみます。

```verilog
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`を指定すると結果も正しくなりました。

```verilog
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クリアしてから渡してくるようで、
たまたま動いているように見えただけでした。