Autodesk Inventor API Hacking (Inventor2025からの注意)

# 0. はじめに
[Freeradical](https://www.freeradical.jp)の中の人、[yamarah](https://qiita.com/yamarah)です。
Inventor2025より、AddInの動作Frameworkが`Net8`になりました。それに伴う注意点をまとめます。

# 1. Inventor2024以前のAddInは動作するの?
後で述べる問題を無視するのなら、よほど特殊なことをしていない普通のAddInであればrecompileなしで動作します。
当然`Net8`にない`NetFramework`固有の機能を用いている場合は、`Net8`に移行して、動作するように修正が必要です。

# 2. 名前空間の分離問題

## 2.1. 問題の概要
Inventor2024以前では`NetFramework`の機能である`AppDomain`を用いて、各AddInの実行空間が分離されていました。ですので、自分以外のAddInが何をしているのか気にする必要はありませんでした。
`AppDomain`の機能は`Net8`に移植されませんでした。ではInventor2025ではどうなっているのかと言うと、全てのAddInが1つの実行空間にloadされます。
これは時にして、問題を生じさせます。例えば、2つのAddInのそれぞれが同じdllのversion違いを参照していると、早い者勝ちで先に評価された方のdllだけが読み込まれ、他方のAddInは自分が想定しているのと別versionのdllを参照することになります。
たまたまうまく動作すれば良いものの、多くの場合においてこれは望ましい動作ではありません。
単一dllでのみ構成されるAddInは、そもそも問題が発生しないので、ここでそっと閉じてください。
## 2.2. 解決策～AssemblyLoadContextを使う
`Net8`には、`AppDomain`に相当する`AssemblyLoadContext`という機能があります。これを使って自分だけの空間を作り、そこに自分自身を読み込むと他のAddInが使っているdllに干渉されずに、自分用に特定のdllを読み込めます。
「そこに自分自身を読み込むと」と書きましたが、実際にはInventorによってloadされた時には手遅れです。ですので、専用のAssemblyLoadContextを作って、そこに本体をloadするloader AddIn(なりすましAddIn)を作る必要があります。
本来はこのloader機構がInventor内部に実装されるべきなのですが、少なくともInventor2025.0.0の時点ではなされていません。
## 2.3. Loader
Loaderのcodeです。

```c#:NetLoader.cs
using Inventor;
using System;
using System.IO;
using System.Linq;
using System.Reflection;

namespace Freeradical.Inventor.NetLoader;

[System.Runtime.InteropServices.Guid(TargetConfig.AddInGuid)]
public class NetLoader : ApplicationAddInServer
{
    private AddInLoadContext? addInLoadContext;
    private ApplicationAddInServer? addInInstance;
    public void Activate(ApplicationAddInSite AddInSiteObject, bool FirstTime)
    {
        if (addInLoadContext is null)
        {
            var appPath = Assembly.GetExecutingAssembly().Location;
            var appDirectory = System.IO.Path.GetDirectoryName(appPath) ?? throw new DirectoryNotFoundException();
            addInLoadContext = new AddInLoadContext(appDirectory);
            addInLoadContext.ForceReloadingAssemblyNames.AddRange(TargetConfig.ForceReloadingAssemblyNames);
        }
        if (addInInstance is null)
        {
            var addIn1Assembly = addInLoadContext.LoadFromAssemblyName(new AssemblyName(TargetConfig.AddInAssemblyName));
            addInInstance = addIn1Assembly.CreateInstance(TargetConfig.AddInInstanceName) as ApplicationAddInServer;
            if (addInInstance is null) throw new InvalidOperationException();
        }
        addInInstance.Activate(AddInSiteObject, FirstTime);
    }

    public void Deactivate()
    {
        addInInstance?.Deactivate();
#if DEBUG
        if (addInLoadContext is null) return;
        System.Diagnostics.Debug.WriteLine($"AddIn name : {TargetConfig.AddInName}");
        System.Diagnostics.Debug.WriteLine($"Resolved dlls ({addInLoadContext.ResolvedAssemblyNames.Count})");
        var formattedNames = addInLoadContext.ResolvedAssemblyNames.Select(name => $"\"{name}\",");
        System.Diagnostics.Debug.WriteLine(string.Join(null, formattedNames));
#endif
    }

    public void ExecuteCommand(int CommandID)
    {
        addInInstance?.ExecuteCommand(CommandID);
    }

    public dynamic? Automation => addInInstance?.Automation;
}
```
`Activate`時に自分専用の`AssemblyLoadContext`を作り、そこに本体をloadし、本体のinstanceを作成します。あとはInventorからloaderが受け取った呼び出しを、本体のinstanceにトンネルします。
このcode自体を汎用化させるため、AddIn固有の設定は別ファイル`TargetConfig.cs`にまとめています。

## 2.4. 設定ファイル
```c#:TargetConfig.cs
namespace Freeradical.Inventor.NetLoader;

internal static class TargetConfig
{
    /// <summary>
    /// TargetのAddIn名
    /// </summary>
    internal static readonly string AddInName = "MyAddIn";
    /// <summary>
    /// manifestに記載されているのと同じGuidを記入すること。
    /// </summary>
    internal const string AddInGuid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
    /// <summary>
    /// 強制的に新しくロードするアセンブリ名
    /// </summary>
    internal static readonly string[] ForceReloadingAssemblyNames = [AddInAssemblyName!, "OtherDllName", "For.Examble.System.Reactive",];

    // オプション
    internal static readonly string AddInAssemblyName = AddInName;
    internal static readonly string AddInInstanceName = $"{AddInName}.StandardAddInServer";
}
```
`ForceRelodingAssemblyNames`には、強制的に自分専用の`AssemblyLoadContext`に読み込むdll名を列挙します。
「こんなの無くても、無条件に上書き読み込みすれば良いじゃないか」と思われるでしょう。この理由は後程説明します。

## 2.5. 専用の`AssemblyLoadContext`
今回は専用の解決ルールをもった`AssemblyLoadContext`が必要だったため、継承して新たなclassを作成しました。
```c#:AddInLoadContext.cs
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;

namespace Freeradical.Inventor.NetLoader;
internal class AddInLoadContext(string addInDirectory) : AssemblyLoadContext
{
    protected readonly string addInDirectory = addInDirectory;
    internal readonly List<string> ForceReloadingAssemblyNames = [];
#if DEBUG
    internal readonly List<string> ResolvedAssemblyNames = [];
#endif
    protected override Assembly? Load(AssemblyName assemblyName)
    {
        var assembly = base.Load(assemblyName);
        if (assembly is not null) return assembly;
        var name = assemblyName.Name;
        if (ForceReloadingAssemblyNames.All(x => !string.Equals(x, name, System.StringComparison.OrdinalIgnoreCase)))
        {
            try
            {
                return Default.LoadFromAssemblyName(assemblyName);
            }
            catch (FileNotFoundException)
            {
            }
        }
        var path = ResolveDllPath(assemblyName);
        if (path is null) return null;
#if DEBUG
        ResolvedAssemblyNames.Add(assemblyName.Name ?? "(null)");
#endif
        return LoadFromAssemblyPath(path);
    }

    protected string? ResolveDllPath(AssemblyName assemblyName)
    {
        var path = Path.Combine(addInDirectory, assemblyName.Name + ".dll");
        return File.Exists(path) ? path : null;
    }
}
```
これは以下の順序でdllを検索します。

1. 既に自分専用`AssemblyLoadContext`に登録されていたら、それを返す。
2. `TargetConfig.ForceReloadingAssemblyNames`にない名前ならば、全てのAddInで共用している`AssemblyLoadContext`を使って名前解決する。
3. `TargetConfig.ForceReloadingAssemblyNames`にある名前、もしくは2.で失敗した場合は、本体AddInと同じdirectoryを検索してloadする。

この手順には意味があります。
* `TargetConfig.ForceReloadingAssemblyNames`を使わず、まずは共用`AssemblyLoadContext`で解決するようにすれば、先に別のAddInでloadされたdllをversionに構わず使うことになる。
* `TargetConfig.ForceReloadingAssemblyNames`を使わず、常に本体AddInと同じdirectoryにあるdllを優先して読み込むと、AddIn云々以前にInventor自体が使っているdllのversionと齟齬が出る場合がある。

つまりは、この専用`AssemblyLoadContext`で読み込むべきdllは以下のものです。
* AddInに関係なくInventor自体が使っているdllは除外した、残りの必要とするdll。

ならば、そもそも除外すべきdllをdirectoryに置かず、同directoryに存在するdllは全て強制的に読み込めば・・・と思うでしょう。確かにInventor2025(つまりは`Net`)専用ならばそれで良いのです。しかし、Inventor2024(同`NetFramework`)と共用しようとすると、それぞれで標準で読み込まれるdllに違いがあり、Inventor2024の場合だけ必要とされるものがあります。
ですので、共用する場合はInventor2024でのみ必要とされるdllもdirectoryに配置し、それらを`TargetConfig.ForceReloadingAssemblyNames`に記載**しない**ことで、loadするdllを制御するわけです。
対象をInventor2025以降に限定するならば、このような複雑な処理は不要なので、適宜codeを修正してください。

## 2.6. `ForceReloadingAssemblyNames`の選定
「そんなこと言っても、`TargetConfig.ForceReloadingAssemblyNames`に記述すべきdllを選定するのって邪魔くさいよね?」
ごもっとっもです。お気づきかと思いますが、上記codeには`#if DEBUG`で括られた部分があります。他の全てのAddInをloadせず、`TargetConfig.ForceReloadingAssemblyNames`を空にして`Debug`でbuildしたAddInだけをloadするようInventorを設定し、Debug起動→終了すると、専用`AssemblyLoadContext`で解決されたdllの一覧が出力されます。
これらはInventorでは標準的にloadされない、すなわち、他のAddInと干渉しうるdllですので、そのままTargetConfig.csにコピペすると良いでしょう。

# 3. InventorのVersion別の*.addin設定
`NetFramework`でbuildされた本体を`MyAddIn`, それの`Net`用loader(なりすましAddIn)を`MyAddInNetLoader`とすると、それぞれに*.addinを記述すると良いです。
```xml:Autodesk.MyAddIn.Inventor.addin
<Addin Type="Standard">
  <ClassId>{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}</ClassId>
  <ClientId>{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}</ClientId>
  <DisplayName>My Add-in</DisplayName>
  <Description>My Add-in</Description>
  <Assembly>MyAddIn.dll</Assembly>
  <LoadOnStartUp>1</LoadOnStartUp>
  <UserUnloadable>1</UserUnloadable>
  <Hidden>0</Hidden>
  <SupportedSoftwareVersionLessThan>29..</SupportedSoftwareVersionLessThan>
  <DataVersion>1</DataVersion>
  <UserInterfaceVersion>1</UserInterfaceVersion>
</Addin>
```
```xml:Autodesk.MyAddInNetLoader.Inventor.addin
<Addin Type="Standard">
  <ClassId>{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}</ClassId>
  <ClientId>{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}</ClientId>
  <DisplayName>My Add-in</DisplayName>
  <Description>My Add-in</Description>
  <Assembly>MyAddInNetLoader.dll</Assembly>
  <LoadOnStartUp>1</LoadOnStartUp>
  <UserUnloadable>1</UserUnloadable>
  <Hidden>0</Hidden>
  <SupportedSoftwareVersionGreaterThan>28..</SupportedSoftwareVersionGreaterThan>
  <DataVersion>1</DataVersion>
  <UserInterfaceVersion>1</UserInterfaceVersion>
</Addin>
```
`SupportedSoftwareVersionLessThan`と`SupportedSoftwareVersionGreaterThan`を使って、対象となるInventorを切り分けるのがミソです。
また、`MyAddIn`と`MyAddInNetLoader`の`Id`は、同じで構いません。というか、同じの方が好ましいです。
また、これらのファイル、MyAddIn.dll, MyAddInNetLoader.dll, そして参照されるdllは全て同じdirectoryに配置して構いません。

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