Autodesk Inventor API Hacking (AddInの動的Load/Unload)

# 0. はじめに
**25.01.04追記**
*今となっては、この方法より[Out processからの操作](https://qiita.com/yamarah/items/eb108bfeb5eb7cf771c3)をお勧めします。*
*また、本記事で扱っているAppDomainは`.NET`環境ではサポートされていません。*
**25.01.04追記終わり**


Inventorは、動的にAddInをLoadできますが、Unloadはできません。
`アドインマネージャー`に`ロード/ロード解除`のチェックボックスがありますが、これはあくまで無効にするだけ(`Deactive()`を呼ぶだけ)で、本当の意味でDLLをunloadしてはいません。ですので、`ロード/ロード解除`を繰り返しても、staticな変数は再初期化されないわけです。
# 1. 動的Load/Unloadのもたらすもの
この記事では、動的にLoad/Unloadする(完全にunloadする)方法について書きますが、そもそも何故そんなことをしたいかと言うと、debugの効率を上げたいからです。
普通にVisualStudioからdebugすると、codeの修正の度にInventorを再起動させなくてはならず、実行場面の再現(Inventorの起動 → 特定のdocumentを開く → 特定の条件を再現)に手間がかかり、これが馬鹿にならないコストなのです。
# 2. 方針
標準の手段で読み込まれたAddInはunloadできないので、「AddInを読み込むAddIn」を作ります。名付けて、「AddIn Hot Loader」です。

```
+----------+  Load        +----------+  Load        +-----------+
| Inventor | -----------> | AddIn    | -----------> | 開発AddIn |
|          |  Activate()  |  Hot     |  Activate()  |           |
|          | -----------> |   Loader | -----------> |           |
+----------+              |          |  Deactivate()|           |
                          |          | -----------> |           |
                          |          |  Unload      |           |
                          |          | -----------> |           |
                          +----------+              +-----------+
```
AddIn Hot LoaderはLoadしっぱなしで、それが別のAddInを動的にLoad/Unloadするわけです。
# 3. 技術的課題
DLLを動的に読み込むことは、さほど技術を必要としません。しかし、解放しようとすると、一気に難易度が上がります。
その細かい内容については、本記事の執筆範囲から外れますので、下記記事を参考にしてください。

[アセンブリを動的にロードし、そして完全にアンロードする](http://gushwell.ldblog.jp/archives/52211840.html)

ざっくり説明すると、AddIn Hot Loader自体が実行されているのとは別の環境(AppDomain)を作って、そこにdebug対象のDLLをloadします。unloadするときは、作成したAppDomainごと破棄します。

しかし、残念ながら1つ問題が発生しました。
AddIn Hot Loaderが実行されているCurrentDomainのBaseDirectoryが、Inventorのbin(具体的には、C:\Program Files\Autodesk\Inventor ####\Bin)を指しているのです。
別のAppDomainを作って、そこにProxyの実体を作るわけですが、そのためにAppDomainはProxyの定義が含まれるAssembly、つまりAddInHotLoader.dllをLoadしないといけません。
(念のために言いますが、ここでいうProxyやAssemblyは、Inventorのそれとは関係ありません。C#の話しです。)
Proxyを作成するためにAddInHotLoader.dllを探すわけですが、Inventorのbinには存在しないので、失敗します。

結論として、私はAddInHotLoader.dllをInventorのbinにcopyしました。余りスマートな解決方法でなく、別の方法を取りたい気持ちは強いです。しかし、かなりの時間を費やしても、元々の場所のAddInHotLoader.dllを使うには、私の技術力は及びませんでした。
失敗に終わりましたが、試した内容としては・・・

* AppDomainSetupを使って、ApplicationBaseを変更する。
* AppDomainの名前解決をhookして、隔離されたDirectoryにあるDLLを読み込む。
([Assembly Loading across AppDomains](https://weblog.west-wind.com/posts/2009/Jan/19/Assembly-Loading-across-AppDomains))

もう少し突っ込んでやれば解決できたのかも知れませんが、またもや力尽きたのです・・・

なお、AddInとしてのAddInHotLoaderも、普段のAddInが配置されているpathからではなく、Inventorのbinにある方が読み込ます。
ですので、通常のAddIn配置場所にはファイル`Autodesk.AddInHotLoader.Inventor.addin`さえ在ればよく、DLLは不要です。
# 4. 作ってみて、使ってみた
![AddInHotLoader.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424028/d9ce624c-f280-e5c2-365b-79060a863602.png)
こんな感じで、Loadを押すと、以下を実行します。

* AppDomainの作成。
* そこにProxyを作成。
* Proxy経由で、debug対象のDLLを読み込み。
* Proxy経由で、Activate()を実行。

Unloadを押すと、以下を実行します。

* Proxy経由で、Deactivate()を実行。
* AppDomainの破棄。

では、VisualStudioでdebugするのは、どうすれば良いでしょうか。

* Inventorを起動。(VisualStudioのDebug経由ではなく、普通に起動する。)
* AddIn Hot Loaderでdebug対象のDLLを読み込む。
* VisualStudioで対象プロジェクトを読み込み、`デバッグ`→`プロセスにアタッチ...`で、実行中のInventorのプロセスを選択する。

これで、debugが出来ます。途中でcodeを修正する場合は、

* VisualStudioで`デバッグ`→`すべてデタッチ`を実行。
* AddIn Hot LoaderでUnloadする。
* codeを修正して、DLLを再作成する。
* AddIn Hot Loaderで作成したDLLを読み込む。
* VisualStudioで`デバッグ`→`プロセスにアタッチ...`を実行。

です。
# 5. Sample code
これだけでは足りませんが、核となる部分のcodeを以下に記します。

```C#:LoadDialog.cs
// 以下のcodeは抜粋で、このままでは動作しません

public partial class LoadDialog : Form
{
    public Inventor.ApplicationAddInSite inventor;
    private AppDomain appDomain;
    private Proxy proxy;

    public LoadDialog()
    {
        InitializeComponent();
        //
        UpdateControlState();
    }

    public void UpdateControlState()
    {
        if (appDomain == null)
        {
            LoadButton.Enabled = File.Exists(DllPathTextBox.Text);
            UnloadButton.Enabled = false;
        }
        else
        {
            LoadButton.Enabled = false;
            UnloadButton.Enabled = true;
        }
    }

    private void ShowErrorDialog(string message, Exception exception = null)
    {
        if (exception != null)
        {
            message += Environment.NewLine + exception.ToString();
        }
        MessageBox.Show(message, "AddIn Hot Loader", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }

    public void UnloadAppDomain()
    {
        if (appDomain != null)
        {
            if (proxy != null && proxy.Activated)
            {
                try
                {
                    proxy.Deactivate();
                }
                catch (Exception ex)
                {
                    ShowErrorDialog("Deactive()で例外が送出されました。", ex);
                }
            }
            try
            {
                AppDomain.Unload(appDomain);
                appDomain = null;
                proxy = null;
            }
            catch (Exception ex)
            {
                ShowErrorDialog("AppDomainの解放に失敗しました。", ex);
            }
        }
    }

    private void LoadButton_Click(object sender, EventArgs e)
    {
        bool isError = false;
        try
        {
            appDomain = AppDomain.CreateDomain("InventorAddInHotLoaderDomain");

            proxy = (Proxy)appDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, typeof(Proxy).FullName);
        }
        catch (Exception ex)
        {
            ShowErrorDialog("Proxyの作成に失敗しました。", ex);
            isError = true;
        }
        if (!isError)
        {
            try
            {
                proxy.Initialize(DllPathTextBox.Text, NameSpaceTextBox.Text, ClassNameTextBox.Text);
            }
            catch (Exception ex)
            {
                ShowErrorDialog("Proxyの初期化に失敗しました。", ex);
                isError = true;
            }
        }
        if (!isError)
        {
            try
            {
                proxy.Activate(inventor, true);
            }
            catch (Exception ex)
            {
                ShowErrorDialog("Activate()で例外が送出されました。", ex);
                isError = true;
            }
        }
        if (isError)
        {
            UnloadAppDomain();
        }
        UpdateControlState();
    }

    private void UnloadButton_Click(object sender, EventArgs e)
    {
        UnloadAppDomain();
        UpdateControlState();
    }

    private void DllSelectButton_Click(object sender, EventArgs e)
    {
        using (var fileDialog = new OpenFileDialog())
        {
            try
            {
                fileDialog.InitialDirectory = Path.GetDirectoryName(DllPathTextBox.Text);
                fileDialog.FileName = Path.GetFileName(DllPathTextBox.Text);
            }
            catch
            {
                var dirInfo = Directory.GetParent(System.Reflection.Assembly.GetExecutingAssembly().Location);
                try
                {
                    dirInfo = dirInfo.Parent;
                }
                catch { }
                fileDialog.InitialDirectory = dirInfo.FullName;
                fileDialog.FileName = string.Empty;
            }
            fileDialog.Filter = "AddIn DLL Files (*.dll)|*.dll";
            fileDialog.Title = "AddInを選択";

            var result = fileDialog.ShowDialog();

            if (result == DialogResult.OK)
            {
                DllPathTextBox.Text = fileDialog.FileName;
            }
        }
    }

    private void DllPathTextBox_TextChanged(object sender, EventArgs e)
    {
        UpdateControlState();
    }
}
```
```C#:Proxy.cs
internal class Proxy : MarshalByRefObject
{
    private dynamic instance;
    public bool Activated = false;

    public void Initialize(string path, string nameSpace, string className)
    {
        Assembly asm = Assembly.LoadFile(path);
        Type type = asm.GetType(nameSpace + "." + className);
        instance = Activator.CreateInstance(type);
    }

    public void Activate(Inventor.ApplicationAddInSite addInSiteObject, bool firstTime)
    {
        instance.Activate(addInSiteObject, firstTime);
        Activated = true;
    }

    public void Deactivate()
    {
        instance.Deactivate();
    }
}
```

# 6. 注意点
## 1. proxyへの引数について
「[アセンブリを動的にロードし、そして完全にアンロードする](http://gushwell.ldblog.jp/archives/52211840.html)」には、以下の記述があります。

>また、"プロキシークラス" とやり取りするパラメータは、
>MarshalByRefObject で継承
>[Serializable] 属性の付与
>のどちらかを満たさなくてはなりません。

今回の`Proxy.Activate()`では、`Inventor.ApplicationAddInSite`型のobjectを渡しており、この制限の対象に入ると思うのですが、そのままで実害は無いようです。
`Inventor.ApplicationAddInSite`がclassやstructではなく、interfaceなので実体がないので、動作しているのかなと想像しています。

## 2. 再Activate時のInventorに対する影響を考慮すること
AddInの異常終了などで、必ずしも`Deactivate()`が実行されるとは限りません。`Activate()`時のInventorに対する操作(RibbonTabの追加など)を`Deactivate()`で修復するのはAddInの振る舞いとして当然として、debug時には`Activate()`が連続して呼ばれた場合でも破綻しないような対策をしておいた方が良いかもしれません。(あくまで、debug用として)
当然ですが、その確認flagをstaticな変数に持たせても意味がありません。
RibbonTabの例で言えば、既にTabが追加されていれば`Activate()`実行済みと判断して、Inventorに対するresource操作をskipすれば良いと思います。

## 3. ～～～.addinは無視される
AddIn Hot Loaderに自分でcodeを記載しない限りは、ファイル`Autodesk.[アドイン名].Inventor.addin`の内容は参照されません。
ですので、例えば現在のInventorが`SupportedSoftwareVersionGreaterThan`の範囲に入ってなくても、AddInを読み込もうとします。

## 4. Inventorに読み込まれないようにする
Inventor起動時に、debug対象のAddInが読み込まれてしまうと、意味がありません。以下のような対策をします。

* AddIn配置場所へのcopyを抑制する
* ファイル`Autodesk.[アドイン名].Inventor.addin`の`SupportedSoftwareVersionGreaterThan`の値を、十分に大きくする。

もちろん、debug終了時は通常の設定に戻すことをお忘れなく。

## 5. AddIn Hot Loaderをrebuildしたら・・・
Inventorのbinにcopyするのを、お忘れなく!!

# 99. 親の記事に戻る
[Autodesk Inventor API Hacking (概略)](https://qiita.com/yamarah/items/cee08eb8f2b7aecbda83)