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です。
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. 設定ファイル
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を作成しました。
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を検索します。
- 既に自分専用
AssemblyLoadContext
に登録されていたら、それを返す。 -
TargetConfig.ForceReloadingAssemblyNames
にない名前ならば、全てのAddInで共用しているAssemblyLoadContext
を使って名前解決する。 -
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を記述すると良いです。
<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>
<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に配置して構いません。