mikeo_410


 任意のディレクトリからアセンブリ(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は登録しない前提です。

  1. EXE1.exeのプロジェクトでASSEM1.dllのプロジェクトを参照している場合は、すべて同じディレクトリにあるときのみ正常に動作します。
  2. 下の例のようにリフレクションを使って「プロジェクトの参照」をしないなら、EXE1.exe と、ASSEM1.dll は異なるディレクトリに置くことができます。

  2のケースでは、ASSEM2.dll は、EXE1.exe と同じディレクトリに置く必要があり、ASSEM1.dll と同じディレクトリに置けないことが残念です。

  EXE1.exe

  1. namespace EXE1
  2. {
  3.     class Program
  4.     {
  5.         static void Main(string[] args)
  6.         {
  7.             Assembly asm = Assembly.LoadFile(@"c:\tmp\DLL_DIR\ASSEM1.dll");
  8.             Type t = asm.GetType("ASSEM1.Assem1");
  9.             MethodInfo mi = t.GetMethod("GetAssem2Property");
  10.             string s = (string)mi.Invoke(null, null);
  11.             Console.WriteLine(s);
  12.         }
  13.     }
  14. }
ASSEM1.dll ASSEM2.dll
  1. namespace ASSEM1
  2. {
  3.     public class Assem1
  4.     {
  5.         public static string GetAssem2Property()
  6.         {
  7.             ASSEM2.TestInterface testInterface
  8.                 = ASSEM2.Assem2.CreateAssem1();
  9.             return testInterface.property1;
  10.         }
  11.     }
  12. }
  1. namespace ASSEM2
  2. {
  3.     public interface TestInterface
  4.     {
  5.         string property1 { get; }
  6.     }
  7.     public class Assem2 : TestInterface
  8.     {
  9.         const string _property1 = "interfaceのテスト";
  10.         public string property1
  11.         {
  12.             get { return _property1; }
  13.         }
  14.         public static TestInterface CreateAssem1()
  15.         {
  16.             return new Assem2();
  17.         }
  18.     }
  19. }

もう少し考えると

すべてリフレクションで

  動的にパスを示す文字列を作って、任意のディレクトリからアセンブリをロードできるので、リフレクションのみで書くならローディングの問題はありません。
  しかし、これは至難の技です。

参照なしにカスタムなクラスや構造体、インタフェースが通すには

  上の例では、ASSEM2.dllが持っている文字列がEXE1.exeまで通っています。これは基本的な型だからで、カスタムな型を「参照」追加なしに通す方法がわかりません。

同じ宣言をプロジェクトごとに行う

  C++なら、共通のヘッダをインクルードして、問題が回避できます。
  同じ発想で、同じ記述をすれば「プロジェクトの参照」をしないことができるか試してみます。
  下のように ASSEM1.dll に直せば、DLLは同じディレクトリに置け、コンパイルも実行できます。しかし、実行時にキャストでエラーになります。どのDLL由来の ASSEM2.TestInterface なのかまで比較されるからだと思います。

  1. namespace ASSEM2
  2. {
  3.     public interface TestInterface
  4.     {
  5.         string property1 { get; }
  6.     }
  7. }
  8. namespace ASSEM1
  9. {
  10.     public class Assem1
  11.     {
  12.         public static string GetAssem2Property()
  13.         {
  14.             Assembly asm = Assembly.LoadFile(@"c:\tmp\DLL_DIR\ASSEM2.dll");
  15.             Type t = asm.GetType("ASSEM2.Assem2");
  16.             MethodInfo mi = t.GetMethod("CreateAssem1");
  17.             ASSEM2.TestInterface testInterface
  18.                 = (ASSEM2.TestInterface)mi.Invoke(null, null);
  19.             return testInterface.property1;
  20.         }
  21.     }
  22. }

共通の定義ファイルのリンク

  2つの .csファイルに同じ記述をしてもダメなことはわかりました。
  同様の発想でもう少し考えてみます。

  1. C#の場合、objができないので、objをリンクする手はない。
  2. 「相互運用機能アセンブリ」の説明に、.il を修正してリンクする例があります。
    この例では、修正の単位、リンクの単位はアセンブリになります。

  このことと、基本的な型なら通ることから、アセンブリの単位で、共通定義ファイルが作れる可能性が推測できます。

  単純に、共通の定義アセンブリを作成して、それぞれ参照に追加した場合、共通の定義アセンブリは、EXEと同じディレクトリに置くと言う制約が残ります。
  共通定義ファイルは、プロジェクトに実態が埋め込まれる必要があります。
  以下のようにすると実現できました。

リンクする共通定義ファイル

  C#のクラスライブラリ・プロジェクトを作成し、以下のように記述します。
  TestInterface.cpp

  1. namespace TESTINTERFACE
  2. {
  3.     [ComImport]
  4.     [Guid("8B252DC5-709F-4FD0-86FD-3CDD50983CD5")]
  5.     public interface TestInterface
  6.     {
  7.         string property1 { get; }
  8.     }
  9. }

  properties\AssemblyInfo.cs に、1行加えます。

  1. [assembly: ImportedFromTypeLib("")]

  ビルド

  Visual Studio でどうすれば良いかはわかりませんが、以下のようにすれば目的を達することができます。

  1. csc /t:library Assem1.cs /link:..\TestInterface\bin\debug\TestInterface.dll
  2. csc /t:library Assem2.cs /link:..\TestInterface\bin\debug\TestInterface.dll
  3. csc /t:exe /out:EXE1.exe properties\assemblyinfo.cs program.cs /link:..\TestInterface\bin\debug\TestInterface.dll

  Visual Studioでの編集では、「参照の追加」を行ってエラーにならないようにして編集しました。
  これは、/reference: オプションに相当し、/link: は実体を含める指定のようです。

  1. namespace ASSEM1
  2. {
  3.     public interface TestInterface
  4.     {
  5.         string property1 { get; }
  6.     }
  7.     public class Assem1
  8.     {
  9.         public static string GetAssem2Property()
  10.         {
  11.             Assembly asm = Assembly.LoadFile(@"c:\tmp\DLL_DIR\ASSEM2.dll");
  12.             Type t = asm.GetType("ASSEM2.Assem2");
  13.             MethodInfo mi = t.GetMethod("CreateAssem1");
  14.             TestInterface testInterface
  15.                 = (TestInterface)mi.Invoke(null, null);
  16.             return testInterface.property1;
  17.         }
  18.     }
  19. }

「同じ宣言をプロジェクトごとに行う」こととの差異

  .il のレベルで考えると、「同じ宣言をプロジェクトごとに行う」のと「共通の定義ファイルのリンク」にはほとんど差がないはずです。
  両者を比較すると、後者には2つの属性が多く設定されることがわかりました。
  CompilerGeneratedAttribute と TypeIdentifierAttribute です。
  後者は、interface宣言の前に[TypeIdentifierAttribute]に加えれば生成できました。

Visual Studio で 「共通の定義ファイルのリンク」と同じことをするには

  共通の定義ファイルのinterface宣言に[TypeIdentifierAttribute]属性を加えれば、各プロジェクトにソースレベルで加えれば(たとえば「リンクとして追加」)をすれば、cscでLINKしたのと同じように動作します。

  1. [ComImport]
  2. [Guid("8B252DC5-709F-4FD0-86FD-3CDD50983CD5")]
  3. [TypeIdentifierAttribute]
  4. public interface TestInterface
  5. {
  6.     string property1 { get; }
  7. }

  この宣言を、TestInterface.cpp として、各プロジェクトに「追加」」「既存の項目」で追加します。
  この時、「リンクとして追加」を行えば一元管理ができます。
  cscでLINKするのと異なり、TestInterface.cpp のDLLは不要です。プロジェクトにする必要はありません。


mikeo_410@hotmail.com