Private ProxyをUnityで実装
はじめに
このライブラリの解説記事です。こちらの記事をご覧ください。
読んだ前提で話を進めます。
この.NET8移行でしか動かないというこのライブラリをUnityに使えるようにしようということで作りました。
UnsafeAccessorを自前で実装する
.NET8の再現
PrivateProxyは.NET8のUnsafeAccessor
という機能を用いているわけですが、2024/1/17現在Unityでは使えません。
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_field1")]
static extern ref int ___field1__(Sample target);
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "PrivateAdd")]
static extern int __PrivateAdd__(Sample target, int x, int y);
しかし、.NET8以前でも使えるみちがあります。それがILとAllow 'unsafe' Code
です。
ILではprivateだろうがinternalだろうがアクセスできます(コンパイラがチェックしないので)。しかし、このままだと実行時エラーがでるのでAllow 'unsafe' Code
で押さえます。
そしてUnityで扱うならILPostProcessorがあるので、Mono.Cecilで関数を書き換えてやれば、OKです。
ILを扱うのはつらいですけど、ここまではそこまで大変ではなかったです。
ここまでの実装ならSource Generatorもほぼそのままでよかったのですが、UnsafeAccessor
にある制限、static class やgenericsもILではできそうだったので、できるとこまで実装しようと思いました。
ILPostProcess static class対応
ここは意外と簡単で、UnsafeAccessor
にtypeof(TargetClass)
を書き込むだけで、ILPostProcessで受け取るだけで簡単でした。
ILPostProcess Generic関数対応
Generic関数をMono.Cecilで呼び出す方法がちょっと複雑だったので書いておきます。
Mono.Cecilちょっとはわかる人向け
元となるMethodReferenceは普通に作り、ParametersもGenericParametersもこのMethodReferenceに追加する。
その後これをもとにGenericInstanceMethodを作成し、GenericArgumentsはここに追加する。
ILPostProcess Generic Class対応
ここがILPostProcessorで一番躓きました。
そのまま関数定義に用いられたGenericParametersはそのまま使うと、うまくいかず、Generic Classの定義のGenericParametersを使うことでとりあえず動きました。
実装はGenericTypeConverter.csをご覧ください
関数に用いるGenericParametersとGeneric Classの定義のGenericParametersで名前が一致している必要があるので、いやですね、、
しかもこの方法だとGenericMethodと共存できず、Generic class のGeneric Methodは未対応です。誰か解決方法を教えて!!!
PrivateProxyのSourceGeneratorをStatic, Generics 対応させる
static class 対応
これはもういいですよね?static を足して、コンストラクタや、AsPrivateProxy()を消すだけ、簡単。
Generic対応
これがやはり難関でした。
Genericsの\<T\>
とかを追加するのはややこしいだけで別に難しくないですけど、つまづきポイントがたくさんありました。
UnboundedGenericな型からはMemberが取れない。
UnboundedGenericな型とはList<>
やDictionary<,>
みたいなやつですね。
属性に書くときはこの状態で書きますが、GetMembersしても何も取れません。
ITypeSymbol.OriginalDefinition
から定義を持ってくることでMemberがとれます。
GenericParameterはPublicではない
GenericParameterのDeclaredAccessibilityはNotApplicableである。
Generic Constraintsもとれる
Generics対応するならConstraintsも対応しなければなりません。
INamedTypeSymbol.TypeParametersもしくはIMethodSymbol.TypeParametersをとり、
そこからさらにConstraintTypesをとることができます。
GenericParameterでもwhere T: unmanaged
するとちゃんとIsUnmanaged
がtrueになるので、同様にclass
、struct
もかけました。
Ref Field対応
上記で完成かと思いきや、実はまだUnityでは使えません、、
なぜならstructのPrivateProxyではref fieldを用いて、変更が元に伝わるようになっているのですが、Unityではref fieldが対応してない!!
じゃあILでなんとかして見せましょう。
まず、前にこれを解決する方法は記事にしています。
しかし、これでは手間がかかります。ライブラリと導入してすぐ解決とはなりません。
そうだByRefernce<T>をFieldにInjectしてしまおう!!となりましたが、ILPostProcessorからは実行時mscorlibが参照できず、mscorlibのinternal classを使うことはできません。
じゃあEditorから実行時mscorlibを渡そうと思うわけですが、そもそも別プロセスなので、受け渡しはファイルと一度経由する必要があります。
ILPostProcessorは別プロセスなのでUnityEditor 名前空間なども使えません。
Pathはどう取得しましょうか。
Project のpathEnvironment.CurrentDirectrory
で取得できますので、そこでやり取りします。
上手くいかない状況やより良い方法があったら教えてください
これで実行時mscorlibを取得できたので、ByRefernce<T>を見つけ、自分で定義したByRefernce<T>のFieldDefeinitionに加えて、参照の授受もILで書いて、ついに完成!!
最後に
ライブラリを書いている途中でPCが壊れるトラブルもありつつ、完成まで大変でした。
mscorlibやGenericsなど不満はまだあるので、ご意見ください。
Discussion