なんか最近流行ってるらしい
浮世の変化には疎いのですが、なんか流行ってるらしいですねRust
。
実はUnity
のネイティブプラグインを作ってみたかったのですが、C
もC++
もやったことない上に勉強する気もないため踏み切れないでいました。
いい機会なのでRust
で作ってみます。
目的
Rust
を使ってみるUnity
のネイティブプラグインを作ってみる- 自分の学習軌道をメモしておく
書いている人
スマホ開発がメインC
とC++
は未経験
値渡しと参照渡しはわかるけどぽいんた? とかいうのはわからん
情報取得
とりあえずインプットします。
Rust
Rustの日本語ドキュメント/Japanese Docs for Rust
プログラミング言語 Rust, 2nd Edition/ The Rust Programming Language, Second Edition
プログラミング言語Rust
The Rust Programming Language
必修言語Rustの他己紹介
Rust についてのメモ
Rustのポインタ(所有権・参照)・可変性についての簡単なまとめ
Rustはこうやって勉強するといいんじゃないか、という一例
Rustのクレート・ツールを探すための情報源
コンパイル通るまで殴ればいい! かんたん!
こんなにやさしいコンパイラ初めて見ました。cargo
による依存関係の管理もよく練れていていい感じです。Kotlin
なんかもそうですが近代の言語はユーザ獲得のため、チュートリアルがおそろしく充実していていいですね。
あとプロジェクト新規作成したらgit
作ってくれるのすごい。
NativePlugin
Rustで実装したアルゴリズムをUnityから使う
C#からC++(DLL)に配列を渡す
How do I get Rust FFI to return array of structs or update memory?
How to return an array of structs from Rust to C#
プラグイン更新のたびにUnityEditor
の再起動が必要とかつらい。
エディタ
intellij
製品じゃないともうなにも書けない。
MEET INTELLIJ RUST
Vector3
を100倍して返す
ひとまず手始めとして、C#
から渡されたVector3
をRust
側で100倍して返します。
とりあえず作る
// こんな感じで構造体を定義#[repr(C)]pubstructVector3{x:f32,y:f32,z:f32,}// 外部公開する関数#[no_mangle]pubexternfnsize_up(v:&Vector3)->Vector3{Vector3{x:v.x*100.0,y:v.y*100.0,z:v.z*100.0}}
[DllImport("libtest")]privatestaticexternVector3size_up(Vector3moto);
さっそく実行してみましょう。
varv3=UnityEngine.Random.insideUnitSphere;varsizeUpV3=size_up(v3);Debug.Log($"{v3} -> {sizeUpV3}");
(-0.1, -0.8, -0.2) -> (254563.7, 0.0, 1402342000000000000000000.0)
……なんか……なんだろう、よくないことが起こっているようですね。
借用をやめる
というわけで、Rust
側を修正します。
#[no_mangle]pubexternfnsize_up(v:Vector3)->Vector3{Vector3{x:v.x*100.0,y:v.y*100.0,z:v.z*100.0}}
引数のv: Vector3
をうっかり拝借していましたが、Rust
内で完結するならばともかくC#
から借りてくるのはあまりにも無茶な話でした。というわけでそのまま渡してみます。
(-0.3, -0.7, 0.2) -> (-26.1, -66.8, 21.3)
……またよくないことが……いや、よく見たら四捨五入してそうな数字です。
nicely formatted
Rider先生にデコンパイルしてもらってVector3
のToString
を覗いてみます。
/// <summary>/// <para>Returns a nicely formatted string for this vector.</para>/// </summary>/// <param name="format"></param>publicoverridestringToString(){returnUnityString.Format("({0:F1}, {1:F1}, {2:F1})",(object)this.x,(object)this.y,(object)this.z);}
nicely formatted string
${\Large なに言うとるがじゃ!!!!!}$
しょうがないのでこんな感じの拡張メソッド定義してありのままの姿を見せてもらうことにします。
staticclassExtensions{publicstaticstringToStringFloat(thisVector3vector3){return$"({vector3.x}, {vector3.y}, {vector3.z})";}}
varv3=UnityEngine.Random.insideUnitSphere;varsizeUpV3=size_up(v3);Debug.Log($"{v3.ToStringFloat()} -> {sizeUpV3.ToStringFloat()}");
(-0.01608862, 0.5905958, 0.7266953) -> (-1.608862, 59.05958, 72.66953)
できました。
気になるのは借用をやめた修正です。C#
側では「もともとのVector3
」「引数としてコピーされたVector3
」の2つがあります。
「もともとのVector3
」はC#
が管理しているからいいとして、Rust
の借用ではないということは「引数としてコピーされたVector3
」をRust
側で開放しちゃってそうな気がしますが、これってC#
側の扱いはどうなっているのでしょうか。extern
だとそのあたり忖度されるんでしょうか。もしくはstruct
なのでC#
から渡すときに値をコピーしてるから大丈夫なのか。まあいいか。
計測
ようやくNative Plugin
を使いたい理由に入ります。Mesh
の頂点座標を基準点からの相対位置に変換する処理ですが、この処理がやや重い……ような気がします。そう頻繁に行う処理でもないので無理に高速化する必要もないのですが、今回はやってみることそのものが目的です。
というわけで、以下のC#
で書かれた関数をRust
側へ計算処理を逃がす関数にするのが今回のゴールです。
publicstaticVector3[]TransWithCsharp(Matrix4x4matrix,IReadOnlyList<Vector3>points){varret=newVector3[points.Count];for(varcount=0;count<points.Count;count++){ret[count]=matrix.MultiplyPoint(points[count]);}returnret;// LINQでこう書くと実際オサレ// return points.Select(matrix.MultiplyPoint).ToArray();}
matrix4x4
C#
からMatrix4x4
を受け取るため、Rust
側で同じ構造体を定義します。
本来ならありものを使うのではなく、C#
側でも自分でちゃんと受け渡すための構造体を定義するべきですが、Vector3
もそのまま渡せたんだからMatrix4x4
も行けるやろ! の精神です。
#[repr(C)]pubstructMatrix4x4{m00:f32,m01:f32,m02:f32,m03:f32,m10:f32,m11:f32,m12:f32,m13:f32,m20:f32,m21:f32,m22:f32,m23:f32,m30:f32,m31:f32,m32:f32,m33:f32,}
ちゃんとRust
側で受け取れているか試すために、以下の関数を作ってC#
と突き合わせてみます。
#[no_mangle]pubexternfnmatrix_add(matrix:Matrix4x4)->Vector3{Vector3{x:matrix.m00,y:matrix.m01,z:matrix.m02}}
varAnchor=newGameObject().transform;Anchor.position=UnityEngine.Random.insideUnitSphere;Anchor.rotation=UnityEngine.Random.rotation;Anchor.localScale=UnityEngine.Random.insideUnitSphere+Vector3.one;varmatrix=Anchor.transform.localToWorldMatrix;vara=matrix_add(matrix);Debug.Log(a.ToStringFloat());varb=newVector3(matrix.m00,matrix.m01,matrix.m02);Debug.Log(b.ToStringFloat());
(-0.9590587, -0.6367525, -0.6988028)
(-0.9590587, -0.2962521, 0.6547196)
最初だけ合っている。ということはつまり構造体のメンバの定義されている順番が違うのでしょう。
再びRider先生にデコンパイルしてもらいます。
publicstructMatrix4x4:IEquatable<Matrix4x4>{[NativeName("m_Data[0]")]publicfloatm00;[NativeName("m_Data[1]")]publicfloatm10;[NativeName("m_Data[2]")]publicfloatm20;[NativeName("m_Data[3]")]publicfloatm30;[NativeName("m_Data[4]")]publicfloatm01;[NativeName("m_Data[5]")]publicfloatm11;[NativeName("m_Data[6]")]publicfloatm21;[NativeName("m_Data[7]")]publicfloatm31;[NativeName("m_Data[8]")]publicfloatm02;[NativeName("m_Data[9]")]publicfloatm12;[NativeName("m_Data[10]")]publicfloatm22;[NativeName("m_Data[11]")]publicfloatm32;[NativeName("m_Data[12]")]publicfloatm03;[NativeName("m_Data[13]")]publicfloatm13;[NativeName("m_Data[14]")]publicfloatm23;[NativeName("m_Data[15]")]publicfloatm33;}
十の位から増えてるの……?
なんか感覚と違いますが、そう定義されている以上はしょうがありません。Rust
側の構造体の定義の順番を変えます。
#[repr(C)]pubstructMatrix4x4{m00:f32,m10:f32,m20:f32,m30:f32,m01:f32,m11:f32,m21:f32,m31:f32,m02:f32,m12:f32,m22:f32,m32:f32,m03:f32,m13:f32,m23:f32,m33:f32,}
(-0.04724042, -0.6401328, 0.3618424)
(-0.04724042, -0.6401328, 0.3618424)
一致しました。Matrix4x4
はちゃんとC#
からRust
に渡せています。
ダブルキャスト
/// <summary>/// <para>Transforms a position by this matrix (generic).</para>/// </summary>/// <param name="point"></param>publicVector3MultiplyPoint(Vector3point){Vector3vector3;vector3.x=(float)((double)this.m00*(double)point.x+(double)this.m01*(double)point.y+(double)this.m02*(double)point.z)+this.m03;vector3.y=(float)((double)this.m10*(double)point.x+(double)this.m11*(double)point.y+(double)this.m12*(double)point.z)+this.m13;vector3.z=(float)((double)this.m20*(double)point.x+(double)this.m21*(double)point.y+(double)this.m22*(double)point.z)+this.m23;floatnum=1f/((float)((double)this.m30*(double)point.x+(double)this.m31*(double)point.y+(double)this.m32*(double)point.z)+this.m33);vector3.x*=num;vector3.y*=num;vector3.z*=num;returnvector3;}
肝心のMultiplyPoint
の処理です。
デコンパイラの結果というのもあると思いますが、なかなかにカオスな計算処理。float -> double -> float
とキャストしている部分をRust
でも再現するかは悩みどころですが、いったんは心を無にしてRust
でも同様の処理を書きます。
#[no_mangle]pubexternfnmultiply_point(m:Matrix4x4,v:Vector3)->Vector3{letx=((m.m00asf64*v.xasf64+m.m01asf64*v.yasf64+m.m02asf64*v.zasf64)+m.m03asf64)asf32;lety=((m.m10asf64*v.xasf64+m.m11asf64*v.yasf64+m.m12asf64*v.zasf64)+m.m13asf64)asf32;letz=((m.m20asf64*v.xasf64+m.m21asf64*v.yasf64+m.m22asf64*v.zasf64)+m.m23asf64)asf32;letnum=(1.0/(m.m30asf64*v.xasf64+m.m31asf64*v.yasf64+m.m32asf64*v.zasf64)+m.m33asf64)asf32;Vector3{x:(x*num),y:(y*num),z:(z*num)}}
varv3=UnityEngine.Random.insideUnitSphere;Anchor=newGameObject().transform;Anchor.position=UnityEngine.Random.insideUnitSphere;Anchor.rotation=UnityEngine.Random.rotation;Anchor.localScale=UnityEngine.Random.insideUnitSphere+Vector3.one;varmatrix=Anchor.transform.localToWorldMatrix;varwithU=matrix.MultiplyPoint(v3);Debug.Log(withU.ToStringFloat());varwithR=multiply_point(matrix,v3);Debug.Log(withR.ToStringFloat());
(-0.02336239, 0.5018276, 0.7009525)
(-Infinity, Infinity, Infinity)
Infinity...
数回繰り返したところ正負は合っているので、キャストに失敗して無限の彼方に辿り着いているようです。
こんなもんの原因追求する気はさらさらないのでRust
のコードをきれいに書き直します。
fnmultiple_float(a:f32,b:f32)->f64{((aasf64)*(basf64))}#[no_mangle]pubexternfnmultiply_point(m:Matrix4x4,v:Vector3)->Vector3{letx=multiple_float(m.m00,v.x)+multiple_float(m.m01,v.y)+multiple_float(m.m02,v.z)+m.m03asf64;lety=multiple_float(m.m10,v.x)+multiple_float(m.m11,v.y)+multiple_float(m.m12,v.z)+m.m13asf64;letz=multiple_float(m.m20,v.x)+multiple_float(m.m21,v.y)+multiple_float(m.m22,v.z)+m.m23asf64;leta=multiple_float(m.m30,v.x)+multiple_float(m.m31,v.y)+multiple_float(m.m32,v.z)+m.m33asf64;letnum=1.0/a;Vector3{x:(x*num)asf32,y:(y*num)asf32,z:(z*num)asf32}}
なんかもっときれいに書けるような、そうでもないような。
ともあれこれを実行してみます。
(-0.1820646, -0.6444009, 0.6140736)
(-0.1820646, -0.6444009, 0.6140736)
一致しました。これでようやく完成です。
実験
さっそくC#
と比べて早いのか遅いのか実験してみます。
privateconstintPointCount=1000000;privateasyncvoidCheck(CancellationTokentoken){varanchor=newGameObject().transform;while(true){anchor.position=UnityEngine.Random.insideUnitSphere;anchor.rotation=UnityEngine.Random.rotation;anchor.localScale=UnityEngine.Random.insideUnitSphere+Vector3.one;varmatrix=anchor.transform.localToWorldMatrix;varrandomVectors=awaitTask.Run(()=>GenerateRandomVectorAsync(_cancellationTokenSource.Token,PointCount),token);// Rustによる変換Profiler.BeginSample("#ByRust");varr=TransByRust(matrix,randomVectors);Profiler.EndSample();// Rust(Releaseビルド)による変換Profiler.BeginSample("#ByRustR");varrr=TransByRustRelease(matrix,randomVectors);Profiler.EndSample();// C#による変換Profiler.BeginSample("#ByCSharp");varc=TransByCsharp(matrix,randomVectors);Profiler.EndSample();Debug.Log(r.Length+" - "+rr.Length+" - "+c.Length);}}// C#による変換privatestaticVector3[]TransByCsharp(Matrix4x4matrix,IReadOnlyList<Vector3>points){varret=newVector3[points.Count];for(varcount=0;count<points.Count;count++){ret[count]=matrix.MultiplyPoint(points[count]);}returnret;}// RustDebugビルドによる変換privatestaticVector3[]TransByRust(Matrix4x4matrix,IReadOnlyList<Vector3>points){varret=newVector3[points.Count];for(varcount=0;count<points.Count;count++){ret[count]=multiply_point(matrix,points[count]);}returnret;}// RustReleaseビルドによる変換privatestaticVector3[]TransByRustRelease(Matrix4x4matrix,IReadOnlyList<Vector3>points){varret=newVector3[points.Count];for(varcount=0;count<points.Count;count++){ret[count]=multiply_point_r(matrix,points[count]);}returnret;}// ランダムなVector3の配列を生成privatestaticTask<Vector3[]>GenerateRandomVectorAsync(CancellationTokencancellationToken,intlength){varrandom=newSystem.Random();varpoints=newVector3[length];for(varcount=0;count<points.Length;count++){cancellationToken.ThrowIfCancellationRequested();// UnityEngine.RandomのAPIはメインスレッドからしか呼べない...// なので無理矢理ランダムなVector3を生成するpoints[count].x=(float)(random.NextDouble()*random.Next(-100,100));points[count].y=(float)(random.NextDouble()*random.Next(-100,100));points[count].z=(float)(random.NextDouble()*random.Next(-100,100));}returnTask.FromResult(points);}
これがプロファイラの結果です。
C#が一番速い……。Rust
のリリースビルドとデバッグビルドで差が出ている以上、NativePlugin
だからプロファイラがおかしくなっているわけでもないようです。
f32
が溢れるようなことはまずないので、Rust
側でキャストを止めてみます。
#[no_mangle]pubexternfnmultiply_point_without_cast(m:Matrix4x4,v:Vector3)->Vector3{letx=m.m00*v.x+m.m01*v.y+m.m02*v.z+m.m03;lety=m.m10*v.x+m.m11*v.y+m.m12*v.z+m.m13;letz=m.m20*v.x+m.m21*v.y+m.m22*v.z+m.m23;leta=1.0/(m.m30*v.x+m.m31*v.y+m.m32*v.z+m.m33);Vector3{x:(x*a),y:(y*a),z:(z*a)}}
ちょっとはやくなってる。
仮説
- 1. UnityのMatrix4x4.MultiplyPointはC++層で実行されている
- デコンパイルするってことはコンパイルされてるんだよねこれ
- 2. C#がmatrix4x4をキャッシュしているのに対し、Rustは毎回受け渡しているため非効率
- これは確実にあるはず
- 3. 言語間で受け渡すコスト > Rustによる恩恵
- 単純な計算処理では意味がなかった
仮説1が一番大きいと思います。わたしが戦っていたのはキャストしまくりのC#
ではなくバチバチにチューニングされたC++
だったのです……たぶん。なので自分で実装した重い処理とかだったら違う結果が出るかもしれません。
仮説2の解決としてVector3[]
の配列を受け渡しできればいいのですが、ポインタがわからないからマーシャリングもわからないので諦めました。Rust
側でポインタを復元する方法もわからないです。
仮説3もわりとありそうな気がしています。いちいち変換している分のコストはかなり大きい……はず。
あと、いくらVector3
とはいえこの数なら結構なGCを誘発していると思うのですが、プロファイラのGC Alloc
はみんないっしょです。NativePlugin
部分に対するプロファイラの動作もいまいち情報がないのでよくわからん。
まとめ
Rust
今更ですがedition
は2018
です。Rust
の学習ですが、ヤバいと噂の所有権は自分はそんなにひっかかりませんでした。でもライフタイムは微妙にまだよくわかってないかもしれない。
あと、エラー処理と並列プログラミングはちらっと読んだだけで何言ってるかまったく理解してないので改めて読もうと思います。Rust
の学習コストは確かに高いですが、コンパイラさんが徹底的にチェックしてくれることで実行時に吹っ飛ぶのを防止してくれるのはとても好きです。
NativePlugin
Unity
+C#
+Rust
の知識を要求されるのつらい。
敗北
プログラミングぢからは高まった気がしますが、結果が出せていません。
しかし現在の自分ではこれ以上は手が出ない……。ポインタを理解するためにC
を諦めてやってみるべきか……。
なにはともあれ今回はここで敗北します。誰かなんか強い人がなんとかしてくれたら嬉しいな! サヨナラ!
とりあえず書いた分は置いておきます。
gist