LoginSignup
0
0

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

Last updated at Posted at 2024-04-11

0. はじめに

Freeradicalの中の人、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です。

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. 設定ファイル

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を作成しました。

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を記述すると良いです。

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>
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>

SupportedSoftwareVersionLessThanSupportedSoftwareVersionGreaterThanを使って、対象となるInventorを切り分けるのがミソです。
また、MyAddInMyAddInNetLoaderIdは、同じで構いません。というか、同じの方が好ましいです。
また、これらのファイル、MyAddIn.dll, MyAddInNetLoader.dll, そして参照されるdllは全て同じdirectoryに配置して構いません。

99. 親の記事に戻る

Autodesk Inventor API Hacking (概略)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0