Maya .net API を自前 AppDomain でラップしようとして失敗した話
とりあえず書いとけばそのうちなんか役に立つかなと。
問題
- .net API を使うと、unloadPlugin しても DLL のハンドルが解放されない
- ハンドルが Maya に握られたままだから上書きできない
- Visual Studio から上書きできないからリビルドできない
- リビルド前に Maya を再起動する必要がある
- \(^o^)/
原因
そもそもなんで DLL のハンドルが解放されないのか?
.net で DLL 弄るときの嵌りどころでもあるんだけど、.net は DLL をロードするときに内部でキャッシュしてるらしく、読み込んだオブジェクトを破棄しても、内部的にはアセンブリを保持した状態が維持されるらしい。ドキュメントには書いてないし GC との絡みがどうなってるのかもわからないんだけど、同じ DLL を複数回ロードするときは、タイミングによらず 2 回目以降が超早かったりする。この挙動自体は、DllImport でも MEF でも System.Reflection.Assembly でも同じ。MEF や Assembly はファイナライザとか破棄用のインターフェイスがあるけど、DllImport に至ってはそもそもそういうのが存在しない。
自分でコード書いてるときのこれの回避法はおおよそシンプルで、新規に AppDomain を作成して、その中で DLL をロードしてやればいい。そうすれば、ドメインを破棄するときに DLL も解放される。というか、ドメインごと破棄しない限り解放されない。
試しに Maya .net API でコマンドプラグインつくって doIt() のなかで AppDomain.CurrentDomain.FriendlyName を取得してみると、見事に "DefaultDomain" となる。これはアプリプロセスがデフォルトで持ってるドメインで、アプリが終了するまで破棄されることはない。結局 Maya が DefaultDomain の中で DLL を抱え込んでるせいで、この問題が起こる。
じゃあ試しに自前でドメインつくって、そのなかで MGlobal.executeCommandStringResult("loadPlugin somePlugin") とかしてみたらどうなるか。somePlugin.InitializePlugin() あたりで CurrentDomain をみてみると、やっぱり DefaultDomain になってる。だめぽ。
なんでこんな実装になってるんだろう?
これはもう完全に自分の予想でしかないんだけど、単純にマーシャリング対応が追いついてないだけのような気はする。ドメインを分けるとそこでメモリとかのリソース空間が分離されちゃうから、両者でこれらを受け渡すにはこれに対応する必要がある。間違いなく膨大な作業量が発生するだろうし、自分が開発者だったら正直あまり考えたくない。。
ちなみに、問題自体はわりと有名で、Maya の開発チーム側でも認識はしてる模様。
NET Api unload/reload plugin? - Autodesk Community
回避策
というわけで、「自前でドメインを作成して、プラグインまるごとそこで抱え込んでみる」というのを試してみた。
発想としてはごくシンプルで、
// `myComand arg1 arg2 -arg3 3` に相当 pluginDebugLoader -nll myCommandPlugin -doCommand myCommand arg1 arg2 -arg3 3; // `createNode myNode` に相当 $node = `pluginDebugLoader -nll myNodePlugin -createNode myNode`; delete $node; pluginDebugLoader -unload -nll myNodePlugin;
みたいなかんじで動作するラッパープラグインをでっちあげる。
このラッパープラグインは、
というふうに動作する。
.nll.dll も中身はふつうの .net DLL だから、System.Reflection.Assembly でそのままロードできるし、「プロセスにアタッチ」も問題なく動作する。ただ、ノードプラグインの自前実行が予想以上に鬼門だった。。
回避策の問題点
Maya .net API には .nll.dll をロードするための API は存在せず、DefaultDomain にロードされてしまう都合上 executeCommand("loadPlugin") も使えない。つまり、自前で Maya のお作法通りに .nll.dll をロードする必要がある。
これがコマンドプラグインの場合は、
- IExtensionPlugin.InitializePlugin()
- MPxCommand.MPxCommand()
- MPxCommand.doIt()
- IExtentionPlugin.UninitializePlugin()
の順で呼んでやれば問題なく動作する。
ノードプラグインで詰んだ。loadPlugin してない状態だから MDagModifier.createNode() は当然動作しないし、MPxNode のコンストラクタと postConstructor() を呼ぶだけだとまともに動作しない。おそらく MFnPlugin.registerNode() に相当する処理が必要なんだけど、本来これは assembly ターゲットに対する属性クラスを使ってのみ行われてる処理らしく、それ以外のインターフェイスが見当たらず、そもそも .net API に MFnPlugin クラスは存在しない。結果、Maya 側でノードに名前を付けられず作成に失敗する、という事態に陥る。
// This line is mandatory to declare a new node type in Maya // You need to change the last 2 parameters with your own // node name and unique ID // Unique node ID can be obtain on http://www.autodesk.com/developmaya #error You need to change the Node name and Unique ID below before continuing, then remove this line. [assembly: MPxNodeClass(typeof(testNode.MyNode), "MyNode", 0x00000001)]
ドキュメント漁ったり適当に API 打ってみたりしたけど、全然駄目だった。
とりあえず
将来的に Maya .net API 側に MFnPlugin が実装されるか、.net framework 側にアセンブリキャッシュの破棄が実装されたら、また試してみたい。