graphics.hatenablog.com

技術系テクニカルアーティストのあれこれ

Maya .net API を自前 AppDomain でラップしようとして失敗した話

とりあえず書いとけばそのうちなんか役に立つかなと。

f:id:hal1932:20141014144339j:plain

問題

  1. .net API を使うと、unloadPlugin しても DLL のハンドルが解放されない
  2. ハンドルが Maya に握られたままだから上書きできない
  3. Visual Studio から上書きできないからリビルドできない
  4. リビルド前に Maya を再起動する必要がある
  5. \(^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;

みたいなかんじで動作するラッパープラグインをでっちあげる。

このラッパープラグインは、

  1. MArgList を自前で解析してマーシャリング or シリアライズ
  2. 新規 AppDomain の作成
  3. 実行したいプラグインの .nll.dll をロードして実行
  4. AppDomain の破棄

というふうに動作する。

.nll.dll も中身はふつうの .net DLL だから、System.Reflection.Assembly でそのままロードできるし、「プロセスにアタッチ」も問題なく動作する。ただ、ノードプラグインの自前実行が予想以上に鬼門だった。。

回避策の問題点

Maya .net API には .nll.dll をロードするための API は存在せず、DefaultDomain にロードされてしまう都合上 executeCommand("loadPlugin") も使えない。つまり、自前で Maya のお作法通りに .nll.dll をロードする必要がある。

これがコマンドプラグインの場合は、

  1. IExtensionPlugin.InitializePlugin()
  2. MPxCommand.MPxCommand()
  3. MPxCommand.doIt()
  4. 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 側にアセンブリキャッシュの破棄が実装されたら、また試してみたい。