任意のディレクトリからアセンブリ(DLL)をロード
R言語のExtensionをC#で書いた場合、C#で作成したDLLは、R.EXEと同じディレクトリに置く必要があります。
これは、DLLでもアセンブリは独自の検索ルールがあり、インストールされていない場合、アプリケーションの起動ディレクトリだけが探されるためのようです。
通常このルールで不便を感じませんが、インタプリタなどのプラグインを作った場合は不便に感じます。
interface宣言に [TypeIdentifierAttribute] を付けることがこの件の解決になるようです。
問題なこと
C#のコンソールアプリケーション EXE1.exe と、2つのクラスライブラリASSEM1.dll と ASSEM2.dll を作ります。
EXE1.exe は、 ASSEM1.dll のメソッドを呼び出し、ASSEM1.dll は ASSEM2.dll を参照しています。
DLLは登録しない前提です。
-
EXE1.exeのプロジェクトでASSEM1.dllのプロジェクトを参照している場合は、すべて同じディレクトリにあるときのみ正常に動作します。
-
下の例のようにリフレクションを使って「プロジェクトの参照」をしないなら、EXE1.exe と、ASSEM1.dll は異なるディレクトリに置くことができます。
2のケースでは、ASSEM2.dll は、EXE1.exe と同じディレクトリに置く必要があり、ASSEM1.dll と同じディレクトリに置けないことが残念です。
EXE1.exe
- namespace EXE1
- {
- class Program
- {
- static void Main(string[] args)
- {
- Assembly asm = Assembly.LoadFile(@"c:\tmp\DLL_DIR\ASSEM1.dll");
- Type t = asm.GetType("ASSEM1.Assem1");
- MethodInfo mi = t.GetMethod("GetAssem2Property");
- string s = (string)mi.Invoke(null, null);
- Console.WriteLine(s);
- }
- }
- }
ASSEM1.dll |
- namespace ASSEM2
- {
- public interface TestInterface
- {
- string property1 { get; }
- }
- public class Assem2 : TestInterface
- {
- const string _property1 = "interfaceのテスト";
- public string property1
- {
- get { return _property1; }
- }
- public static TestInterface CreateAssem1()
- {
- return new Assem2();
- }
- }
- }
|
もう少し考えると
すべてリフレクションで
動的にパスを示す文字列を作って、任意のディレクトリからアセンブリをロードできるので、リフレクションのみで書くならローディングの問題はありません。
しかし、これは至難の技です。
参照なしにカスタムなクラスや構造体、インタフェースが通すには
上の例では、ASSEM2.dllが持っている文字列がEXE1.exeまで通っています。これは基本的な型だからで、カスタムな型を「参照」追加なしに通す方法がわかりません。
同じ宣言をプロジェクトごとに行う
C++なら、共通のヘッダをインクルードして、問題が回避できます。
同じ発想で、同じ記述をすれば「プロジェクトの参照」をしないことができるか試してみます。
下のように ASSEM1.dll に直せば、DLLは同じディレクトリに置け、コンパイルも実行できます。しかし、実行時にキャストでエラーになります。どのDLL由来の ASSEM2.TestInterface なのかまで比較されるからだと思います。
- namespace ASSEM2
- {
- public interface TestInterface
- {
- string property1 { get; }
- }
- }
- namespace ASSEM1
- {
- public class Assem1
- {
- public static string GetAssem2Property()
- {
- Assembly asm = Assembly.LoadFile(@"c:\tmp\DLL_DIR\ASSEM2.dll");
- Type t = asm.GetType("ASSEM2.Assem2");
- MethodInfo mi = t.GetMethod("CreateAssem1");
- ASSEM2.TestInterface testInterface
- = (ASSEM2.TestInterface)mi.Invoke(null, null);
- return testInterface.property1;
- }
- }
- }
共通の定義ファイルのリンク
2つの .csファイルに同じ記述をしてもダメなことはわかりました。
同様の発想でもう少し考えてみます。
-
C#の場合、objができないので、objをリンクする手はない。
-
「相互運用機能アセンブリ」の説明に、.il を修正してリンクする例があります。
この例では、修正の単位、リンクの単位はアセンブリになります。
このことと、基本的な型なら通ることから、アセンブリの単位で、共通定義ファイルが作れる可能性が推測できます。
単純に、共通の定義アセンブリを作成して、それぞれ参照に追加した場合、共通の定義アセンブリは、EXEと同じディレクトリに置くと言う制約が残ります。
共通定義ファイルは、プロジェクトに実態が埋め込まれる必要があります。
以下のようにすると実現できました。
リンクする共通定義ファイル
C#のクラスライブラリ・プロジェクトを作成し、以下のように記述します。
TestInterface.cpp
- namespace TESTINTERFACE
- {
- [ComImport]
- [Guid("8B252DC5-709F-4FD0-86FD-3CDD50983CD5")]
- public interface TestInterface
- {
- string property1 { get; }
- }
- }
properties\AssemblyInfo.cs に、1行加えます。
- [assembly: ImportedFromTypeLib("")]
ビルド
Visual Studio でどうすれば良いかはわかりませんが、以下のようにすれば目的を達することができます。
- csc /t:library Assem1.cs /link:..\TestInterface\bin\debug\TestInterface.dll
- csc /t:library Assem2.cs /link:..\TestInterface\bin\debug\TestInterface.dll
- csc /t:exe /out:EXE1.exe properties\assemblyinfo.cs program.cs /link:..\TestInterface\bin\debug\TestInterface.dll
Visual Studioでの編集では、「参照の追加」を行ってエラーにならないようにして編集しました。
これは、/reference: オプションに相当し、/link: は実体を含める指定のようです。
- namespace ASSEM1
- {
- public interface TestInterface
- {
- string property1 { get; }
- }
- public class Assem1
- {
- public static string GetAssem2Property()
- {
- Assembly asm = Assembly.LoadFile(@"c:\tmp\DLL_DIR\ASSEM2.dll");
- Type t = asm.GetType("ASSEM2.Assem2");
- MethodInfo mi = t.GetMethod("CreateAssem1");
- TestInterface testInterface
- = (TestInterface)mi.Invoke(null, null);
- return testInterface.property1;
- }
- }
- }
「同じ宣言をプロジェクトごとに行う」こととの差異
.il のレベルで考えると、「同じ宣言をプロジェクトごとに行う」のと「共通の定義ファイルのリンク」にはほとんど差がないはずです。
両者を比較すると、後者には2つの属性が多く設定されることがわかりました。
CompilerGeneratedAttribute と TypeIdentifierAttribute です。
後者は、interface宣言の前に[TypeIdentifierAttribute]に加えれば生成できました。
Visual Studio で 「共通の定義ファイルのリンク」と同じことをするには
共通の定義ファイルのinterface宣言に[TypeIdentifierAttribute]属性を加えれば、各プロジェクトにソースレベルで加えれば(たとえば「リンクとして追加」)をすれば、cscでLINKしたのと同じように動作します。
- [ComImport]
- [Guid("8B252DC5-709F-4FD0-86FD-3CDD50983CD5")]
- [TypeIdentifierAttribute]
- public interface TestInterface
- {
- string property1 { get; }
- }
この宣言を、TestInterface.cpp として、各プロジェクトに「追加」」「既存の項目」で追加します。
この時、「リンクとして追加」を行えば一元管理ができます。
cscでLINKするのと異なり、TestInterface.cpp のDLLは不要です。プロジェクトにする必要はありません。
|