Unity で C ネイティブプラグインから __m128(i/d) 値を送受信する方法

はじめに

C プラグイン側にこういう関数があって、これを Unity (C#) 側から呼びたいとします。

__declspec(dllexport) __m128d nextDouble2(rng_t *rng, __m128d min, __m128d max)  
{  
    // do something...  
}  

なお、 __m128ddouble 型を 2 つつなげた SIMD 用の型です。 float 4 つの __m128uint64_t 2 つの __m128i もあります。

Burst のヘルプ を見ると、

For all DllImport and internal calls, you can only use the following types as parameter or return types:
...
Unity.Burst.Intrinsics.v128

とあるので、 Burst を入れて v128 型で受ければよさそうに思えます。
v128 は 128 bit の union っぽい構造体で、例えば v128.ULong0 で下位 64 bit の ulong 値を取得できたりします。

というわけで C# 側で DllImport を書きましょう:

// not working :(  
[DllImport("DllName")]  
private static extern v128 nextDouble2(IntPtr rng, v128 min2, v128 max2);  

ところがこれは動きません。
呼び出すとランダムな値が返ったり、 Unity がフリーズしたりします。

解決策

前述のヘルプの下のほうを見ると、

Passing structs by value isn't supported; you need to pass them through a pointer or reference.

というわけで、サポートされていません。
いや私もこの部分は読んでいたのですが、まさか v128 も対象だとは思わず…
だって v128 (__m128) はビルトイン型みたいなものじゃないですか…

というわけで、呼べるようにするためにはポインタか ref 参照にしなければなりません。
ポインタにすると unsafe になって面倒なので、 ref でやります。

まずは C プラグイン側にラッパーを生やします。

__declspec(dllexport) void nextDouble2ForUnity  
    (rng_t* rng, __m128d* min2, __m128d* max2, __m128d* output) {  
    *output = nextDouble2(rng, *min2, *max2);  
}  

そして、 C# 側からこのようにして参照します。

// it works fine :)  
[DllImport("DllName")]  
private static extern void nextDouble2ForUnity  
    (IntPtr rng, in v128 min2, in v128 max2, out v128 output);  

ref の代わりに inout を使うこともできるので、適宜使い分けます。

最後に C# 側のラッパーを書けば完成です。

private static v128 nextDouble2(IntPtr rng, v128 min2, v128 max2)  
{  
    nextDouble2ForUnity(rng, min2, max2, out var result);  
    return result;  
}  

おわりに

ググっても何も出てこないのでさっぱりわからず、 n 週間塩漬けにしていました…
私のようにうっかり勘違いをする人がいるかもしれないので残しておきます…