なんか最近流行ってるらしい
浮世の変化には疎いのですが、なんか流行ってるらしいですね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)]
pub struct Vector3 {
x: f32,
y: f32,
z: f32,
}
// 外部公開する関数
# [no_mangle]
pub extern fn size_up(v: &Vector3) -> Vector3 {
Vector3 { x: v.x * 100.0, y: v.y * 100.0, z: v.z * 100.0 }
}
[DllImport("libtest")]
private static extern Vector3 size_up(Vector3 moto);
さっそく実行してみましょう。
var v3 = UnityEngine.Random.insideUnitSphere;
var sizeUpV3 = size_up(v3);
Debug.Log($"{v3} -> {sizeUpV3}");
(-0.1, -0.8, -0.2) -> (254563.7, 0.0, 1402342000000000000000000.0)
……なんか……なんだろう、よくないことが起こっているようですね。
借用をやめる
というわけで、Rust
側を修正します。
# [no_mangle]
pub extern fn size_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>
public override string ToString()
{
return UnityString.Format("({0:F1}, {1:F1}, {2:F1})", (object) this.x, (object) this.y, (object) this.z);
}
nicely formatted string
${\Large なに言うとるがじゃ!!!!!}$
しょうがないのでこんな感じの拡張メソッド定義してありのままの姿を見せてもらうことにします。
static class Extensions
{
public static string ToStringFloat(this Vector3 vector3)
{
return $"({vector3.x}, {vector3.y}, {vector3.z})";
}
}
var v3 = UnityEngine.Random.insideUnitSphere;
var sizeUpV3 = 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
側へ計算処理を逃がす関数にするのが今回のゴールです。
public static Vector3[] TransWithCsharp(Matrix4x4 matrix, IReadOnlyList<Vector3> points)
{
var ret = new Vector3[points.Count];
for (var count = 0; count < points.Count; count++)
{
ret[count] = matrix.MultiplyPoint(points[count]);
}
return ret;
// LINQでこう書くと実際オサレ
// return points.Select(matrix.MultiplyPoint).ToArray();
}
matrix4x4
C#
からMatrix4x4
を受け取るため、Rust
側で同じ構造体を定義します。
本来ならありものを使うのではなく、C#
側でも自分でちゃんと受け渡すための構造体を定義するべきですが、Vector3
もそのまま渡せたんだからMatrix4x4
も行けるやろ! の精神です。
# [repr(C)]
pub struct Matrix4x4 {
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]
pub extern fn matrix_add(matrix: Matrix4x4) -> Vector3 {
Vector3 { x: matrix.m00, y: matrix.m01, z: matrix.m02 }
}
var Anchor = new GameObject().transform;
Anchor.position = UnityEngine.Random.insideUnitSphere;
Anchor.rotation = UnityEngine.Random.rotation;
Anchor.localScale = UnityEngine.Random.insideUnitSphere + Vector3.one;
var matrix = Anchor.transform.localToWorldMatrix;
var a = matrix_add(matrix);
Debug.Log(a.ToStringFloat());
var b = new Vector3(matrix.m00, matrix.m01, matrix.m02);
Debug.Log(b.ToStringFloat());
(-0.9590587, -0.6367525, -0.6988028)
(-0.9590587, -0.2962521, 0.6547196)
最初だけ合っている。ということはつまり構造体のメンバの定義されている順番が違うのでしょう。
再びRider先生にデコンパイルしてもらいます。
public struct Matrix4x4 : IEquatable<Matrix4x4>
{
[NativeName("m_Data[0]")]
public float m00;
[NativeName("m_Data[1]")]
public float m10;
[NativeName("m_Data[2]")]
public float m20;
[NativeName("m_Data[3]")]
public float m30;
[NativeName("m_Data[4]")]
public float m01;
[NativeName("m_Data[5]")]
public float m11;
[NativeName("m_Data[6]")]
public float m21;
[NativeName("m_Data[7]")]
public float m31;
[NativeName("m_Data[8]")]
public float m02;
[NativeName("m_Data[9]")]
public float m12;
[NativeName("m_Data[10]")]
public float m22;
[NativeName("m_Data[11]")]
public float m32;
[NativeName("m_Data[12]")]
public float m03;
[NativeName("m_Data[13]")]
public float m13;
[NativeName("m_Data[14]")]
public float m23;
[NativeName("m_Data[15]")]
public float m33;
}
十の位から増えてるの……?
なんか感覚と違いますが、そう定義されている以上はしょうがありません。Rust
側の構造体の定義の順番を変えます。
# [repr(C)]
pub struct Matrix4x4 {
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>
public Vector3 MultiplyPoint(Vector3 point)
{
Vector3 vector3;
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;
float num = 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;
return vector3;
}
肝心のMultiplyPoint
の処理です。
デコンパイラの結果というのもあると思いますが、なかなかにカオスな計算処理。
float -> double -> float
とキャストしている部分をRust
でも再現するかは悩みどころですが、いったんは心を無にしてRust
でも同様の処理を書きます。
# [no_mangle]
pub extern fn multiply_point(m: Matrix4x4, v: Vector3) -> Vector3 {
let x = ((m.m00 as f64 * v.x as f64 + m.m01 as f64 * v.y as f64 + m.m02 as f64 * v.z as f64) + m.m03 as f64) as f32;
let y = ((m.m10 as f64 * v.x as f64 + m.m11 as f64 * v.y as f64 + m.m12 as f64 * v.z as f64) + m.m13 as f64) as f32;
let z = ((m.m20 as f64 * v.x as f64 + m.m21 as f64 * v.y as f64 + m.m22 as f64 * v.z as f64) + m.m23 as f64) as f32;
let num = (1.0 / (m.m30 as f64 * v.x as f64 + m.m31 as f64 * v.y as f64 + m.m32 as f64 * v.z as f64) + m.m33 as f64) as f32;
Vector3 { x: (x * num), y: (y * num), z: (z * num) }
}
var v3 = UnityEngine.Random.insideUnitSphere;
Anchor = new GameObject().transform;
Anchor.position = UnityEngine.Random.insideUnitSphere;
Anchor.rotation = UnityEngine.Random.rotation;
Anchor.localScale = UnityEngine.Random.insideUnitSphere + Vector3.one;
var matrix = Anchor.transform.localToWorldMatrix;
var withU = matrix.MultiplyPoint(v3);
Debug.Log(withU.ToStringFloat());
var withR = multiply_point(matrix, v3);
Debug.Log(withR.ToStringFloat());
(-0.02336239, 0.5018276, 0.7009525)
(-Infinity, Infinity, Infinity)
Infinity...
数回繰り返したところ正負は合っているので、キャストに失敗して無限の彼方に辿り着いているようです。
こんなもんの原因追求する気はさらさらないのでRust
のコードをきれいに書き直します。
fn multiple_float(a: f32, b: f32) -> f64 {
((a as f64) * (b as f64))
}
# [no_mangle]
pub extern fn multiply_point(m: Matrix4x4, v: Vector3) -> Vector3 {
let x = multiple_float(m.m00, v.x) + multiple_float(m.m01, v.y) + multiple_float(m.m02, v.z) + m.m03 as f64;
let y = multiple_float(m.m10, v.x) + multiple_float(m.m11, v.y) + multiple_float(m.m12, v.z) + m.m13 as f64;
let z = multiple_float(m.m20, v.x) + multiple_float(m.m21, v.y) + multiple_float(m.m22, v.z) + m.m23 as f64;
let a = multiple_float(m.m30, v.x) + multiple_float(m.m31, v.y) + multiple_float(m.m32, v.z) + m.m33 as f64;
let num = 1.0 / a;
Vector3 { x: (x * num) as f32, y: (y * num) as f32, z: (z * num) as f32 }
}
なんかもっときれいに書けるような、そうでもないような。
ともあれこれを実行してみます。
(-0.1820646, -0.6444009, 0.6140736)
(-0.1820646, -0.6444009, 0.6140736)
一致しました。これでようやく完成です。
実験
さっそくC#
と比べて早いのか遅いのか実験してみます。
private const int PointCount = 1000000;
private async void Check(CancellationToken token)
{
var anchor = new GameObject().transform;
while (true)
{
anchor.position = UnityEngine.Random.insideUnitSphere;
anchor.rotation = UnityEngine.Random.rotation;
anchor.localScale = UnityEngine.Random.insideUnitSphere + Vector3.one;
var matrix = anchor.transform.localToWorldMatrix;
var randomVectors = await Task.Run(() => GenerateRandomVectorAsync(_cancellationTokenSource.Token, PointCount), token);
// Rustによる変換
Profiler.BeginSample("#ByRust");
var r = TransByRust(matrix, randomVectors);
Profiler.EndSample();
// Rust(Releaseビルド)による変換
Profiler.BeginSample("#ByRustR");
var rr = TransByRustRelease(matrix, randomVectors);
Profiler.EndSample();
// C#による変換
Profiler.BeginSample("#ByCSharp");
var c = TransByCsharp(matrix, randomVectors);
Profiler.EndSample();
Debug.Log(r.Length + " - " + rr.Length + " - " + c.Length);
}
}
// C#による変換
private static Vector3[] TransByCsharp(Matrix4x4 matrix, IReadOnlyList<Vector3> points)
{
var ret = new Vector3[points.Count];
for (var count = 0; count < points.Count; count++)
{
ret[count] = matrix.MultiplyPoint(points[count]);
}
return ret;
}
// RustDebugビルドによる変換
private static Vector3[] TransByRust(Matrix4x4 matrix, IReadOnlyList<Vector3> points)
{
var ret = new Vector3[points.Count];
for (var count = 0; count < points.Count; count++)
{
ret[count] = multiply_point(matrix, points[count]);
}
return ret;
}
// RustReleaseビルドによる変換
private static Vector3[] TransByRustRelease(Matrix4x4 matrix, IReadOnlyList<Vector3> points)
{
var ret = new Vector3[points.Count];
for (var count = 0; count < points.Count; count++)
{
ret[count] = multiply_point_r(matrix, points[count]);
}
return ret;
}
// ランダムなVector3の配列を生成
private static Task<Vector3[]> GenerateRandomVectorAsync(CancellationToken cancellationToken, int length)
{
var random = new System.Random();
var points = new Vector3[length];
for (var count = 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));
}
return Task.FromResult(points);
}
これがプロファイラの結果です。

C#が一番速い……。
Rust
のリリースビルドとデバッグビルドで差が出ている以上、NativePlugin
だからプロファイラがおかしくなっているわけでもないようです。
f32
が溢れるようなことはまずないので、Rust
側でキャストを止めてみます。
# [no_mangle]
pub extern fn multiply_point_without_cast(m: Matrix4x4, v: Vector3) -> Vector3 {
let x = m.m00 * v.x + m.m01 * v.y + m.m02 * v.z + m.m03;
let y = m.m10 * v.x + m.m11 * v.y + m.m12 * v.z + m.m13;
let z = m.m20 * v.x + m.m21 * v.y + m.m22 * v.z + m.m23;
let a = 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