Unity (#1) Advent Calendar 第1日目を飾るにはものすごくふさわしくない超地味な内容となっています。申し訳ございません。
以前に
Unity(WebGL)でC#の関数からブラウザー側のJavaScript関数を呼び出すまたはその逆(JS⇒C#)に関する知見(プラグイン形式[.jslib])
という長ったらしいタイトルの記事を書きましたが、今回はこれの更新版+αという内容となっています。
前回の記事はもう古くなってしまったので、改めて調査をしました。
(古い記事は一応古いバージョンとして残しておきます)
調査したUnityのバージョン: 2019.4.1f1 (と2020.2.0b2.3094)
(文中ではそれぞれ 2019, 2020 と省略して表記します)
どうしてもES6+でコードが書けない
なんかemscriptenの最新バージョンだとES6+でコードが書けるようになったとかならないとか。ただしUnityで使用されているemscriptenではいまだ(Unity 2020)にES6+でコード書くと怒られます。
同じ関数名の関数はどちらかが上書きされる
複数の.jslibを用意してコンパイルすると、同じスコープ(ブロック)に展開されます。
なので、別々の.jslibファイルであっても同じ関数名の関数を定義した場合、どちらかが上書きされてしまいますので注意が必要です。ですので、名前が被らないような少し長めな関数名にすることがいいでしょう。
(どのような順番で上書きされるのかまでは未調査)
特に、アセットストアにあるWebGL用のアセットをインポートすると高確率で.jslibファイルがありますので、もしかするとこういったアセットの.jslibの関数を上書きしてしまう可能性があることに注意してください。
逆に、これがとても有効に働くときもあります。それがUnity自身が用意している.jslib(実際は.js)の上書きです。
例えば、WebCamTextureのWebGLビルド用ソースはWebCam.jsというファイルになっています。ただ、このWebCam.jsは複数カメラが接続された状態でのカメラの選択などが行えないなどの非情なまでのバグがあります。このWebCam.jsで定義されている関数と同じ関数名で正しく動作する関数を定義した.jslibファイルを用意してコンパイルすればきちんとカメラデバイスを選択できるようになります。
きちんとデバイスを選択できるようにしたサンプル
dynCallパターン
dynCallのパターンが2019では 165パターンに結構増えており、さらに2020においては566パターンとめちゃくちゃ増えてます。
ちなみに、C:\Program Files\Unity\Hub\Editor[version]\Editor\Data\PlaybackEngines\WebGLSupport\BuildTools\Emscripten にあるemscripten-version.txtの内容を見ると2019, 2020ともに"1.38.11"となっており一緒でした。
同じバージョンなのになぜパターン数が違うのかが疑問です。
dynCallデータ型に'j'が追加される(2019~)
データ型に'j'が追加されています。
ドキュメントから引用させていただくと
- 'v': void type
- 'i': 32-bit integer type
- 'j': 64-bit integer type (currently does not exist in JavaScript)
- 'f': 32-bit float type
- 'd': 64-bit float type
となっており'j'はBigIntとして扱うのでしょう。
(なぜ'j'なのかをDiscordで聞いてみたら'i'の次だからそうです)
とすると、HEAP64やHEAPU64があるのかと予想しましたが2020でもありませんでした。
このIssueの最後の開発者コメントで、WASM_BIGINTフラグを有効にすることでHEAP64が追加されるということです。
調べてみるとWASM_BIGINTフラグは1.39.13で追加されたもので、試せる環境が手元にないため未検証です。
Runtimeオブジェクトの廃止(2019~)
古いバージョンでは、dynCall()などのメソッドはRuntimeオブジェクトにありましたが、このRuntimeが廃止されているようで見当たりませんでした。dynCall()も見当たりません。ですので、dynCall()の代わりに直接dynCall_viといったパターン分用意されたメソッドを使用します。
// ptrCSFuncは、C#側関数のポインターRuntime.dynCall('vii',ptrCSFunc,[arg1,arg2]);
Module.dynCall_vii(ptrCSFunc,strPtr1,strPtr2);
数値配列を渡す(引数)、数値配列を戻す(戻り値)
配列を引数に渡すと.jslib側ではポインターとして受け取ります。ですのでポインターから配列に戻す処理が必要です。
戻り値として配列を戻す場合は、_malloc()したポインターで戻すということをしなければなりません。
固定長配列でしたら、それほど苦労せずに受け渡すことができますが、問題は可変長配列の場合です。
特に戻り値として戻す場合は、1つのデータにしなければなりません。
配列の最初の要素に要素数を追加するという方法も考えたのですが、バイト配列だと最大でも要素数が256までになってしまいますのでこの方法はあまり有効ではありません。頑張って導き出した答えが、最初の4バイトを要素数にし以降を配列のデータとすることでとりあえずできました。
unsafeを使えばある程度すっきりしたコードになりパフォーマンスも上がりますが、ここではあえて(皆さん嫌いな)Marshalを使った方法をとってみました。
可変長配列を受け取り、可変長配列を戻すサンプルコード
// Unity[DllImport("__Internal")]privatestaticexternIntPtrbyteArrayFunc(byte[]arg,intlength);privatestaticbyte[]ptrToByteArray(IntPtrptr){intlen=Marshal.ReadInt32(ptr);byte[]arr=newbyte[len];Marshal.Copy(IntPtr.Add(ptr,4),arr,0,len);returnarr;}privatestaticvoidtest(){// byte[]を渡し、byte[]の戻り値を受け取るbyte[]byteArrayArg=newbyte[]{1,2,3};IntPtrptrByteArray=byteArrayFunc(byteArrayArg,byteArrayArg.Length);byte[]byteArrayRet=ptrToByteArray(ptrByteArray);Debug.Log($"byteArrayFunc ret: [{string.Join(", ",byteArrayRet.Select(x=>x.ToString()).ToArray())}]");}
// .jslib$utils:{arrayToReturnPtr:function(arr,type){varbuf=(newtype(arr)).buffer;varui8a=newUint8Array(buf);varptr=_malloc(ui8a.byteLength+4);HEAP32.set([arr.length],ptr>>2);HEAPU8.set(ui8a,ptr+4);returnptr;},},byteArrayFunc:function(arg,len){debugger;varbyteArray=HEAPU8.subarray(arg,arg+len);console.log('byteArrayFunc arg: '+utils.arrayToString(byteArray));varret=[3,2,1];varptr=utils.arrayToReturnPtr(ret,Uint8Array);returnptr;}
_free()するタイミング
前述のサンプルコードを見ていただくと一つ問題に気付いた方もいると思います。
_malloc()したのですから_free()しなければいけません。
戻り値として_malloc()したポインターを戻す場合、いつ_free()するかという問題にあたります。
return ステートメント以降で行わないといけないですが、当然returnステートメント以降は実行されません。
簡単な方法としてはsetTimeout()を使って_free()を実行することで一応、回避可能です。
// .jslib//前述のサンプルコードのarrayToReturnPtr関数部分arrayToReturnPtr:function(arr,type){varbuf=(newtype(arr)).buffer;varui8a=newUint8Array(buf);varptr=_malloc(ui8a.byteLength+4);HEAP32.set([arr.length],ptr>>2);HEAPU8.set(ui8a,ptr+4);// setTimeout()で_free()を行うsetTimeout(function(){_free(ptr)},0);returnptr;},//...
もっと確実な方法としては、面倒ではありますが.jslib側に_free()を行う関数を用意しておき、C#側から戻り値を受け取り用が済んだらその関数を実行することです。
// Unity// _free()を行う関数追加[DllImport("__Internal")]privatestaticexternvoidexecFree(uintarg);[DllImport("__Internal")]privatestaticexternIntPtrbyteArrayFunc(byte[]arg,intlength);privatestaticbyte[]ptrToByteArray(IntPtrptr){intlen=Marshal.ReadInt32(ptr);byte[]arr=newbyte[len];Marshal.Copy(IntPtr.Add(ptr,4),arr,0,len);// 用が済んだら_free()を行うexecFree((uint)ptr);returnarr;}privatestaticvoidtest(){// バイト配列を渡し、バイト配列の戻り値を受け取るbyte[]byteArrayArg=newbyte[]{1,2,3};IntPtrptrByteArray=byteArrayFunc(byteArrayArg,byteArrayArg.Length);byte[]byteArrayRet=ptrToByteArray(ptrByteArray);Debug.Log($"byteArrayFunc ret: [{string.Join(", ",byteArrayRet.Select(x=>x.ToString()).ToArray())}]");}
// .jslib// _free()を行う関数を追加execFree(ptr){_free(ptr);}byteArrayFunc:function(arg,len){debugger;varbyteArray=HEAPU8.subarray(arg,arg+len);console.log('byteArrayFunc arg: '+utils.arrayToString(byteArray));varret=[3,2,1];varptr=utils.arrayToReturnPtr(ret,Uint8Array);returnptr;}
可変長の文字列配列を渡す、文字列配列を戻す
じゃあ、可変長数値配列の受け渡しができたなら文字列配列も受け渡しできたい。文字コードはUTF8で。
数値配列の受け渡しを応用すれば一応
できました。
// Unity[DllImport("__Internal")]privatestaticexternvoidexecFree(uintarg);[DllImport("__Internal")]privatestaticexternIntPtrstringArrayFunc(string[]arg,intlength);privatestaticbyte[]ptrToByteArray(IntPtrptr){Debug.Log($"ptr: {(uint)ptr}");intlen=Marshal.ReadInt32(ptr);Debug.Log($"byteArry len:{len}");byte[]arr=newbyte[len];Marshal.Copy(IntPtr.Add(ptr,4),arr,0,len);execFree((uint)ptr);returnarr;}privatestaticstring[]ptrToStringArray(IntPtrptr){intlen=Marshal.ReadInt32(ptr);Debug.Log($"stringArry len:{len}");IntPtr[]ptrArr=newIntPtr[len];Debug.Log(ptrArr);Marshal.Copy(IntPtr.Add(ptr,4),ptrArr,0,len);List<string>ret=newList<string>();for(vari=0;i<len;i++){varbyteArray=ptrToByteArray(ptrArr[i]);varstr=Encoding.UTF8.GetString(byteArray);ret.Add(str);}execFree((uint)ptr);returnret.ToArray();}publicstaticvoidtest(){string[]stringArrayArg=newstring[]{"foo","bar","baz"};IntPtrptrStringArray=stringArrayFunc(stringArrayArg,stringArrayArg.Length);string[]stringArrayRet=ptrToStringArray(ptrStringArray);Debug.Log($"stringArrayFunc ret: [{string.Join(", ",stringArrayRet)}]");}
// .jslibstringArrayFunc:function(arg,len){varstrArray=[];for(vari=0;i<len;i++){varptr=HEAP32[(arg>>2)+i];varstr=Pointer_stringify(ptr);strArray.push(str);}console.log('strArrayFunc arg: '+strArray);varret=['hoge','fuga','piyo','hogera','ほげほげ','叱る'];varretPtr=utils.stringArrayToReturnPtr(ret);returnretPtr;}
見ていただくとわかる通り、可変長数値配列の受け渡しもそうですが、可変長文字列配列の受け渡しはさらにめんどいことに。はっきり言ってJSONで受け渡したほうが楽です。
文字列を_malloc()した場合は、Unity側で自動で_free()してくれるのですが、C#側でUTF8に変換したいためにbyte[]に変換しているため自動で_free()されません。
(Marshal.PtrToStringAnsi()で一応、ポインターから文字列に変換することは可能ですがUTF16に変換されてしまいます。.NET5ではMarshal.PtrToStringUTF8()というまんまな関数が用意されましたが、いかんせんUnityでの.NET5のサポートはまだまだ先になるようです)
固定長数値配列の参照渡し
UnityのWebXR Exporterというアセットのソースを覗いてたら、お!っと思うコードが記述されていました。
// Unity[DllImport("__Internal")]privatestaticexternvoidrefArrayFunc(float[]a,intl);int[]refIntArray=newint[3];refArrayFunc(refFloatArray,a.Length);
// .jslibrefIntArrayFunc:function(arg,len){Module.refIntArray=newInt32Array(buffer,arg,len);}
このように書くことで、C#側のrefFloatArrayと.jslib側のModule.refFloatArrayは参照渡しの関係となり、.jslib側でModule.refFloatArrayの値を変更すると、(returnステートメントなしに)C#のrefFloatArrayに値が反映されます。
テクスチャー
テクスチャーは、C#側で生成し、Texture.GetNativeTexturePtr()でポインターを取得し、ポインターを.jslibの関数に渡す。.jslib側でGL.textures[ptr]でテクスチャーを参照することが可能"らしいです"
。"らしいです"
というのは、C#で
vartexture=newTexture2D(0,0,TextureFormat.ARGB32,false);varptr=texture.GetNativeTexturePtr();
としても、ptrは0になり有効な値になってくれません。"もし、有効なポインターを取得する方法をご存じの方がいらっしゃればぜひご教授をお願いします"
仮に有効なポインターの値が取得できた場合は
// .jslibtextureFunc(ptr){GLctx.bindTexture(GLctx.TEXTURE_2D,GL.textures[ptr]);GLctx.pixelStorei(GLctx.UNPACK_FLIP_Y_WEBGL,true);GLctx.texImage2D(GLctx.TEXTURE_2D,0,GLctx.RGBA,GLctx.RGBA,GLctx.UNSIGNED_BYTE,video);GLctx.pixelStorei(GLctx.UNPACK_FLIP_Y_WEBGL,false);}
といったコードを書くことにより、そのテクスチャーにimgエレメントの画像や、videoエレメントの映像、WebRTCなどのMediaStreamの映像などもほぼ直接的に表示できるようになる"はずです"
。
最後に
Unity (#1) Advent Calendar 第1日目の内容は以上となります。
ちょっとネタに走った感はありますが、.jslibを書けるようになればUnityだけではできないこと、特にJS(Web)のいろんなAPIなどをUnityに取り入れることが可能となりますのでぜひかけるようになりましょう!
あ、あとまとめたテストコードも載せておきます
// UnityusingAOT;usingSystem;usingSystem.Collections;usingSystem.Collections.Generic;usingSystem.Linq;usingSystem.Reflection;usingSystem.Runtime.InteropServices;usingSystem.Text;usingUnityEngine;publicclassjslibtest:MonoBehaviour{[DllImport("__Internal")]privatestaticexternvoidexecFree(uintarg);[DllImport("__Internal")]privatestaticexternbytebyteFunc(bytearg);[DllImport("__Internal")]privatestaticexternshortshortFunc(shortarg);[DllImport("__Internal")]privatestaticexternintintFunc(intarg);[DllImport("__Internal")]privatestaticexternfloatfloatFunc(floatarg);[DllImport("__Internal")]privatestaticexterndoubledoubleFunc(doublearg);[DllImport("__Internal")]privatestaticexternIntPtrbyteArrayFunc(byte[]arg,intlength);[DllImport("__Internal")]privatestaticexternIntPtrshortArrayFunc(short[]arg,intlength);[DllImport("__Internal")]privatestaticexternIntPtrintArrayFunc(int[]arg,intlength);[DllImport("__Internal")]privatestaticexternIntPtrfloatArrayFunc(float[]arg,intlength);[DllImport("__Internal")]privatestaticexternIntPtrdoubleArrayFunc(double[]arg,intlength);[DllImport("__Internal")]privatestaticexternIntPtrstringArrayFunc(string[]arg,intlength);[DllImport("__Internal")]privatestaticexternvoidrefIntArrayFunc(int[]arr,intlen);privateint[]refIntArray=newint[3];privatevoidStart(){test();refIntArrayFunc(refIntArray,refIntArray.Length);StartCoroutine(chekRefArray());}IEnumeratorchekRefArray(){while(true){yieldreturnnewWaitForSeconds(0.3f);Debug.Log($"refIntArray: [{string.Join(", ",refIntArray.Select(x=>$"{x}"))}]");}}privatevoidUpdate(){}privatestaticbyte[]ptrToByteArray(IntPtrptr){Debug.Log($"ptr: {(uint)ptr}");intlen=Marshal.ReadInt32(ptr);Debug.Log($"byteArry len:{len}");byte[]arr=newbyte[len];Marshal.Copy(IntPtr.Add(ptr,4),arr,0,len);execFree((uint)ptr);returnarr;}privatestaticshort[]ptrToShortArray(IntPtrptr){intlen=Marshal.ReadInt32(ptr);Debug.Log($"shortArry len:{len}");short[]arr=newshort[len];Marshal.Copy(IntPtr.Add(ptr,4),arr,0,len);returnarr;}privatestaticint[]ptrToIntArray(IntPtrptr){intlen=Marshal.ReadInt32(ptr);Debug.Log($"intArry len:{len}");int[]arr=newint[len];Marshal.Copy(IntPtr.Add(ptr,4),arr,0,len);returnarr;}privatestaticfloat[]ptrToFloatArray(IntPtrptr){intlen=Marshal.ReadInt32(ptr);Debug.Log($"floatArry len:{len}");float[]arr=newfloat[len];Marshal.Copy(IntPtr.Add(ptr,4),arr,0,len);returnarr;}privatestaticdouble[]ptrToDoubleArray(IntPtrptr){intlen=Marshal.ReadInt32(ptr);Debug.Log($"doubleArry len:{len}");double[]arr=newdouble[len];Marshal.Copy(IntPtr.Add(ptr,4),arr,0,len);returnarr;}privatestaticstring[]ptrToStringArray(IntPtrptr){intlen=Marshal.ReadInt32(ptr);Debug.Log($"stringArry len:{len}");IntPtr[]ptrArr=newIntPtr[len];Debug.Log(ptrArr);Marshal.Copy(IntPtr.Add(ptr,4),ptrArr,0,len);List<string>ret=newList<string>();for(vari=0;i<len;i++){varbyteArray=ptrToByteArray(ptrArr[i]);varstr=Encoding.UTF8.GetString(byteArray);ret.Add(str);}execFree((uint)ptr);returnret.ToArray();}publicstaticvoidtest(){bytebyteArg=210;bytebyteRet=byteFunc(byteArg);Debug.Log($"byteFunc ret: {byteRet}");shortshortArg=210;shortshortRet=shortFunc(shortArg);Debug.Log($"shortFunc ret: {shortRet}");intintArg=210;intintRet=intFunc(intArg);Debug.Log($"intFunc ret: {intRet}");floatfloatArg=210.123f;floatfloatRet=floatFunc(floatArg);Debug.Log($"floatFunc ret: {floatRet}");doubledoubleArg=210.321d;doubledoubleRet=doubleFunc(doubleArg);Debug.Log($"doubleFunc ret: {doubleRet}");byte[]byteArrayArg=newbyte[]{1,2,3};IntPtrptrByteArray=byteArrayFunc(byteArrayArg,byteArrayArg.Length);byte[]byteArrayRet=ptrToByteArray(ptrByteArray);Debug.Log($"byteArrayFunc ret: [{string.Join(", ",byteArrayRet.Select(x=>$"{x}"))}]");short[]shortArrayArg=newshort[]{4,5,6};IntPtrptrShortArray=shortArrayFunc(shortArrayArg,shortArrayArg.Length);short[]shortArrayRet=ptrToShortArray(ptrShortArray);Debug.Log($"shortArrayFunc ret: [{string.Join(", ",shortArrayRet.Select(x=>$"{x}"))}]");int[]intArrayArg=newint[]{7,8,9};IntPtrptrIntArray=intArrayFunc(intArrayArg,intArrayArg.Length);int[]intArrayRet=ptrToIntArray(ptrIntArray);Debug.Log($"intArrayFunc ret: [{string.Join(", ",intArrayRet.Select(x=>$"{x}"))}]");float[]floatArrayArg=newfloat[]{1.1f,2.2f,3.3f};IntPtrptrFloatArray=floatArrayFunc(floatArrayArg,floatArrayArg.Length);float[]floatArrayRet=ptrToFloatArray(ptrFloatArray);Debug.Log($"floatArrayFunc ret: [{string.Join(", ",floatArrayRet.Select(x=>$"{x}"))}]");double[]doubleArrayArg=newdouble[]{5.5d,6.6d,7.7d};IntPtrptrDoubleArray=doubleArrayFunc(doubleArrayArg,doubleArrayArg.Length);double[]doubleArrayRet=ptrToDoubleArray(ptrDoubleArray);Debug.Log($"doubleArrayFunc ret: [{string.Join(", ",doubleArrayRet.Select(x=>$"{x}"))}]");string[]stringArrayArg=newstring[]{"foo","bar","baz"};IntPtrptrStringArray=stringArrayFunc(stringArrayArg,stringArrayArg.Length);string[]stringArrayRet=ptrToStringArray(ptrStringArray);Debug.Log($"stringArrayFunc ret: [{string.Join(", ",stringArrayRet)}]");}}
varlib={$utils:{arrayToString:function(arr){varret='[';for(vari=0;i<arr.length;i++){varspl=i===arr.length-1?'':', ';ret+=arr[i].toString()+spl;}returnret+']';},arrayToReturnPtr:function(arr,type){varbuf=(newtype(arr)).buffer;varui8a=newUint8Array(buf);varptr=_malloc(ui8a.byteLength+4);HEAP32.set([arr.length],ptr>>2);HEAPU8.set(ui8a,ptr+4);// setTimeout(function() { _free(ptr) }, 0);returnptr;},stringArrayToReturnPtr:function(strArr){varptrArray=[];varenc=newTextEncoder();for(vari=0;i<strArr.length;i++){varbyteArray=enc.encode(strArr[i]);varptr=utils.arrayToReturnPtr(byteArray,Uint8Array);ptrArray.push(ptr);}varptr=utils.arrayToReturnPtr(ptrArray,Uint32Array);returnptr;}},execFree:function(ptr){console.log('free ptr: '+ptr);_free(ptr);},byteFunc:function(arg){console.log('byteFunc arg: '+arg);varret=128;returnret;},shortFunc:function(arg){console.log('shortFunc arg: '+arg);varret=128;returnret;},intFunc:function(arg){console.log('intFunc arg: '+arg);varret=128;returnret;},longFunc:function(arg){console.log('longFunc arg: '+arg);varret=128;returnret;},floatFunc:function(arg){console.log('floatFunc arg: '+arg);varret=128.123;returnret;},doubleFunc:function(arg){console.log('doubleFunc arg: '+arg);varret=128.123;returnret;},byteArrayFunc:function(arg,len){varbyteArray=HEAPU8.subarray(arg,arg+len);console.log('byteArrayFunc arg: '+utils.arrayToString(byteArray));varret=[3,2,1];varptr=utils.arrayToReturnPtr(ret,Uint8Array);console.log('jslib ptr: '+ptr);returnptr;},shortArrayFunc:function(arg,len){varshortArray=HEAP16.subarray(arg,len);console.log('shortArrayFunc arg: '+shortArray);varret=[6,5,4];varptr=utils.arrayToReturnPtr(ret,Int16Array);returnptr;},intArrayFunc:function(arg,len){varintArray=HEAP32.subarray(arg,len);console.log('intArrayFunc arg: '+intArray);varret=[9,8,7];varptr=utils.arrayToReturnPtr(ret,Int32Array);returnptr;},floatArrayFunc:function(arg,len){varfloatArray=HEAPF32.subarray(arg,len);console.log('floatFunc arg: '+floatArray);varret=[3.3,2.2,1.1];varptr=utils.arrayToReturnPtr(ret,Float32Array);returnptr;},doubleArrayFunc:function(arg,len){vardoubleArray=HEAPF64.subarray(arg,len);console.log('doubleFunc arg: '+doubleArray);varret=[6.6,5.5,4.4,3.3,2.2];varptr=utils.arrayToReturnPtr(ret,Float64Array);returnptr;},stringArrayFunc:function(arg,len){varstrArray=[];for(vari=0;i<len;i++){varptr=HEAP32[(arg>>2)+i];varstr=Pointer_stringify(ptr);strArray.push(str);}console.log('strArrayFunc arg: '+strArray);varret=['hoge','fuga','piyo','hogera','ほげほげ','叱る'];varretPtr=utils.stringArrayToReturnPtr(ret);returnretPtr;},refIntArrayFunc:function(arg,len){console.log('ref len:'+len);Module.refIntArray=newInt32Array(buffer,arg,len);Module.sampleValue=0;setInterval(function(){console.log('refIntArray update: '+Module.refIntArray.length+''+Module.sampleValue);for(vari=0;i<Module.refIntArray.length;i++){Module.refIntArray[i]=Module.sampleValue+i;}Module.sampleValue+=Module.refIntArray.length;},1000);}};autoAddDeps(lib,'$utils');mergeInto(LibraryManager.library,lib);