◆哀丁・四方山話 第11話
JavaからC#のDLLを呼び出す
ジャンル・キーワード
- Windows 10
- Visual Studio 2019
- JNR(JNI)
- C#
結果は・・・・解決(^o^)
何が起こったの?
3つの異なる世界
Windows上で動作する多くのプログラムは、Microsoftが提供している.NET Frameworkの上で稼働します。このようなプログラムの事を「マネージドコード」と呼びます。
一方で旧来の言語(VB6など)や、Microsoft系ではない言語(Cなど)で作成されたプログラムは、「.NETではない世界」で動作します。これを「アンマネージドコード(ネイティブコード)」と呼びます。
私が日頃利用しているPROCESSINGも、Java上(Java VM)で稼働するプログラムですので、.NETではない世界で動作していますね。Javaの場合は、マネージドでもネイティブでもない世界(Java VMの中)で動作します。
このようにWindowsの世界には、マネージドコード、アンマネージドコード、それ以外(Javaの世界)という3つの異なる世界が広がっているのです。
(画像URL:illust-AC 様:ダニエルさん、acworksさん)
3つの世界は仲が悪い
Javaは歴史がある言語で、強力なライブラリーや拡張機能が豊富に用意されていますが、ときにOSやプラットホーム固有の機能を生かしたプログラムが作りたくなる事もあります。
そんなときWindowsであれば、Microsoftが用意しているAPI(Win32API)を利用するか、.NETフレームワークが用意している命令を利用して、OS固有の機能を呼び出す事になります。
ですがアンマネージドコードで作られたプログラムからは、マネージドコードで作られたプログラムを利用することができません。
またJavaはMicrosoftではない団体が管理している言語であるため、マネージドコードだけでなく、アンマネージドコード(Win32APIで作られた部品)も呼び出すことができないのです。
図にすると、こんな関係になります。そうです。3つの世界はあまり仲が良くなかったんですね・・・。
仲良くしようよ
これではあまりにも不便なので、JNI(Java Native Interface)という仕組みが考え出されました。
JNIを使うと、Javaからアンマネージドなプログラムを呼び出すことが可能となります。実際にはJNIは少し使いにくい面があるため、JNIを改良したJNA(Java Native Access)を使うことがよくあります。
こんなイメージですね。はい。JNA(JNI)は、Javaとアンマネージドコードの2つの世界の橋渡しをする人なんですね。いいヤツなんです(笑)。
(画像URL:illust-AC 様:ダニエルさん、acworksさん)
それでも困った事がある
しかし、やっぱりJavaからは、.NETワールドにあるマネージドコードは利用することができません。JNAとはいえ、完璧ではないんですね。
こんな時、どーしても.NETワールドのプログラムを呼び出したい場合はどうするのかというと・・・
代表的な方法としては、Visual C++を使ってCLI(.NET Frameworkの共通言語基盤)を利用した「アンマネージドな世界とマネージドな世界を橋渡しする部品」を自分たちで作成し、その部品経由でマネージドコードを利用することになります。
こんな感じです。
C++ CLIで作成された橋渡し用の部品(ラッパー関数)は、JNAからも呼び出す事が可能です。
(画像URL:illust-AC 様:ダニエルさん、acworksさん)
これでようやく、3つの世界をなんとか繋ぐことができました。
ところが、このC++/CLIを「自作しなければいけない」というところが曲者で、はっきり言って初心者向きではありません。いやプロでも面倒くさいです(汗)。
でも、いままではここが精一杯でした。
救世主現る
ながーい前フリが終わって、ようやく本題に入ります。
このC++/CLIの橋渡し用部品を用意しなくても、JNAからマネージドコードの部品を呼び出す画期的な方法を見つけました。それが「DllExport」というC#のライブラリーです。
「DllExport」を導入すると、以下のような制約条件は付きますが、C++/CLIで橋渡し用の部品を作成するのに比べて、圧倒的に少ない手数でJavaからC#で作られたマネージドなDLLを呼び出すことが可能となります。
- 呼び出される側(C#)のソースコードに若干手を加える必要がある
- 呼び出されるC#側の関数は、static 関数である事
- DllExportで公開指定された関数から、DllExportで公開指定された別関数は呼び出せない
- DllからDllを呼び出すのが難しい
詳しく書くと、上図のような関係になります。
図で Exportと書かれた関数が、DllExportにより公開指定された関数です。これをC++/CLIの橋渡し部品を用意する事なく、呼び出せるようになります。
(画像URL:illust-AC 様:ダニエルさん、acworksさん)
ただし・・・、(私の技術力が足りないせいなのですが)呼び出し元となるDllから、別のDllにある部品を呼び出す事に苦戦しました。図でいうと、緑の点線の箇所(関数A->関数D、関数C->関数Eなど)です。
呼び出す方法はあるのですが、普段利用しない方法で呼び出す事しかできませんでした・・・orz。それでもよければ、この記事の一番下の方で紹介していますので参考としてください。
結論
DllExportは大変便利です。DllExportは、VisualStudioからNuGet経由でインストールする事が可能です。
以下に、最も基本的な導入方法と利用例を掲載しておきます。
1.C#でマネージドなDLLを作成する
以下のように、.NET FrameworkのDLLを作成します。
プロジェクトを作成したら、試験的に以下のようなコードを作成してください。namespaceとclass名は、あなたのプロジェクトに合わせて変更してくださいね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
using System.IO; using System.Linq; using System.Reflection; using System.Windows.Forms; namespace FirstLibraly { public class FirstClass { [DllExport] public static int ExportStaticFuncA(int val) { return val + 100; } [DllExport] public static string ExportStaticFuncB(string val) { MessageBox.Show(val); return string.Format("{0}を表示", val); } } } |
ポイントは、アンマネージド側に公開する関数に[DllExport]というアノテーションを付けることです。
あとは注意書きでも書いたように、公開する関数は static にしてください。
この時点では[DllExport]部分に赤い波線が出てコンパイルできませんが、気にしないでください。
2.NuGetでDllExportをインストールする
プロジェクト->NuGetパッケージの管理を選択します。
参照で検索窓にDllExportと入力し、見つけたDllExportパッケージをインストールします。
インストール途中で、以下のようなダイアログボックスが開きます。
作成するC#のDLLのbit数を選択(32bitならx86、64bitならx64、両方なら(x86+64))し、Installedにチェックをつけ、Applyを押下します。
注意点はbit数の選択です。
ここで仮に64bitを選択したら、呼び出し側(Java側)も64bitで作成する必要があります。JDKやPROCESSING、EclipseやIntelliJ IDEAなどのIDEのビット数もすべて64bitで統一してください。
64bitと32bitの混在はできない事に注意してください。
インストールが終わると、以下のようなダイアログボックスが開くので「すべて再読み込み」を選択します。
先程まで表示されていた[DllExport]部分の赤い波線が消えているはずです。ビルドして、DLLを作成してください。
3.Java側を作成する
Java側をIntelliJ IDEAで作成する例を紹介します。
まずはIDEで新規プロジェクトを作成します。JNAパッケージをインストールしたかったので私はgradleを使いましたが、ここはみなさんの好みで変えてください。
JNAはmvnrepository から入手可能です。2020/03月現在、Ver5.5.0が最新です。またJava Native Access Platformも入手しておいてください。
gradleを使っている人は、新規プロジェクトのbuild.gradleに以下の記述を追加してください。
1 2 3 4 5 6 7 8 |
dependencies { // https://mvnrepository.com/artifact/net.java.dev.jna/jna compile group: 'net.java.dev.jna', name: 'jna', version: '5.5.0' // https://mvnrepository.com/artifact/net.java.dev.jna/jna-platform compile group: 'net.java.dev.jna', name: 'jna-platform', version: '5.5.0' //↑上記2つの compile指定を追加する testCompile group: 'junit', name: 'junit', version: '4.12' } |
続いて以下のようにクラスを記述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
package SamplePkg; import com.sun.jna.Library; import com.sun.jna.Native; import com.sun.jna.NativeLibrary; public class CallTest { private static NETDLL INSTANCE = null; //DLLライブラリポインタ private final static String JNA_CODENAME = "shift_jis"; private final static String DLLNAME = "FirstLibraly.dll"; /** * C# DLL 公開関数インタフェース */ interface NETDLL extends Library { int ExportStaticFuncA(int val ); String ExportStaticFuncB(String val ); } public static void main(String[] args) { //DLLの場所 String libPath = "C:\\IdeaPrj\\CallTest\\lib"; //JNAの文字コードをshift_jisにする(文字化け防止) System.setProperty("jna.encoding", JNA_CODENAME ); //DLLを読み込む NativeLibrary.addSearchPath( DLLNAME, libPath ); INSTANCE = (NETDLL)Native.load(DLLNAME, NETDLL.class); //関数呼び出し int retInt = INSTANCE.ExportStaticFuncA(100); System.out.println("ExportStaticFuncA ret=" + retInt ); String retStr = INSTANCE.ExportStaticFuncB("ほげ"); System.out.println("ExportStaticFuncB ret=" + retStr ); } } |
インタフェースの名前は、自由につけてもらって構いません。インタフェース部に、C#側で公開した関数を定義します。
main()の中でJNAにDLLを読み込ませ、インタフェースに定義した関数を呼び出しています。
ポイントは
1 2 |
//JNAの文字コードをshift_jisにする(文字化け防止) System.setProperty("jna.encoding", JNA_CODENAME ); |
と書かれた箇所です。
JNAはJavaのシステムエンコードを見て動作するようですが、(なぜか)shift_jis以外の場合、C#側とやり取りする文字列が化けてしまいます。なので強制的に、JNAが利用する文字コードを shift_jisにしています。
実行すると、「ほげ」と表示された小さなメッセージBOXが表示されます。
またIntelliJ IDEAのコンソールに
のようなメッセージが表示されて、みごとにJava->C#のDLLを呼び出せたことがわかります。
やったね!
わかった事
DllExportを利用すると、C++/CLIでブリッジ用のDLLを用意する事なく、簡単にC#のDLLを利用する事ができそうです。
ここでは簡単な例のみを紹介しましたが、がんばればかなり複雑な情報の交換もできるようです。必要に迫られている方は以下の参考サイト様などをご覧になると、幸せになれるかもしれません。
おまけ:DLLから別のDLLを呼び出す
そもそもは・・・DLL(1段目)から別のDLL(2段目)を呼びだす事自体、結構難易度が高い作業となります。
1段目のDLLは呼び出せても、2段目のDLLが見つからないなどの問題によく出くわします(汗)。特にDllExportを利用した場合は、2段目のDLLが見えずに苦戦しました。
以下に紹介する方法が「正解」なのかどうかはわかりませんが、これしか方法を思いつきませんでした(汗)。
それは、2段目のDLLを1段目のDLLから動的リンクで呼び出す方法です。
参考までに、私が試行錯誤した方法を記載しておきます。誰かの参考になれば幸いです。
まず2段目のDLLを作成する
検証するために、2段目のDLLを以下のように作成し、ビルドしてDLLを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
using System.Windows.Forms; namespace SecondLibraly { public class SecondClass { public static void ExportStaticFuncC(string val) { MessageBox.Show(val); return; } public int PublicFuncD(int val) { return val += 200; } } } |
ExportStaticFuncCはstaticな公開関数、PublicFuncDは通常の公開関数です。2段目のDLLはJavaから直接呼び出さないため、DllExportは利用していません。
1段目の DLLから動的リンクで呼び出す
1段目のDLLを、以下のように改造します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
using System; using System.IO; using System.Linq; using System.Reflection; using System.Windows.Forms; namespace FirstLibraly { public class FirstClass { [DllExport] public static int ExportStaticFuncA(int val) { return val + 100; } [DllExport] public static string ExportStaticFuncB(string val) { MessageBox.Show(val); return string.Format("{0}を表示", val); } //ここから下を追加 private const string DLL_PATH = @"C:\IdeaPrj\CallTest\lib"; private const string DLL_NAME = "SecondLibraly.dll"; private const string NAME_SPACE = "SecondLibraly"; private const string CLASS_NAME = "SecondClass"; [DllExport] public static void CallStaticSecondFunc(string val) { //アセンブリ情報の読み込み var asm = Assembly.LoadFrom(Path.Combine(DLL_PATH, DLL_NAME)); if (asm == null) { Console.WriteLine("LoadFrom error"); return; } //指定オブジェクトをアセンブリ情報から取得 var type = asm.GetType( string.Format("{0}.{1}", NAME_SPACE, CLASS_NAME)); if (type == null) { Console.WriteLine("GetType error"); return; } //指定されたStatic関数をオブジェクトから取り出す var method = type.GetMethods() .FirstOrDefault(s => s.Name.Equals("ExportStaticFuncC")); if (method != null && method.IsStatic && method.IsPublic) { //関数を呼び出す method.Invoke(asm, new object[] { val } ); return; } return ; } [DllExport] public static void CallPublicSecondFunc(int val) { //アセンブリ情報の読み込み var asm = Assembly.LoadFrom(Path.Combine(DLL_PATH, DLL_NAME)); if (asm == null) { Console.WriteLine("LoadFrom error"); return; } //指定オブジェクトをアセンブリ情報から取得 var type = asm.GetType( string.Format("{0}.{1}", NAME_SPACE, CLASS_NAME)); if (type == null) { Console.WriteLine("GetType error"); return; } //オブジェクトをインスタンスとして生成 dynamic p1 = Activator.CreateInstance(type); //Public関数を呼び出す int retVal = p1.PublicFuncD( val ); //結果表示 MessageBox.Show(string.Format("{0}", retVal)); return ; } } } |
2段目のDLLに含まれているstatic関数(ExportStaticFuncC)とPublic関数(PublicFuncD)を呼び出す処理を追加しています。
static関数は2段目のDLLに含まれているものを直接呼び出すことが可能です(スタティックですからね!)が、Public関数の方はインスタンスを生成してから呼び出す必要があります。
Java側も修正する
それでは呼び出すJava側のモジュールも修正しましょう。以下のようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
package SamplePkg; import com.sun.jna.Library; import com.sun.jna.Native; import com.sun.jna.NativeLibrary; public class CallTest { private static NETDLL INSTANCE = null; //DLLライブラリポインタ private final static String JNA_CODENAME = "shift_jis"; private final static String DLLNAME = "FirstLibraly.dll"; /** * C# DLL 公開関数インタフェース */ interface NETDLL extends Library { int ExportStaticFuncA(int val ); String ExportStaticFuncB(String val ); //以下を追加 void CallStaticSecondFunc(String val); void CallPublicSecondFunc(int val ); } public static void main(String[] args) { //DLLの場所 String libPath = "C:\\IdeaPrj\\CallTest\\lib"; //JNAの文字コードをshift_jisにする(文字化け防止) System.setProperty("jna.encoding", JNA_CODENAME ); //DLLを読み込む NativeLibrary.addSearchPath( DLLNAME, libPath ); INSTANCE = (NETDLL)Native.load(DLLNAME, NETDLL.class); //関数呼び出し int retInt = INSTANCE.ExportStaticFuncA(100); System.out.println("ExportStaticFuncA ret=" + retInt ); String retStr = INSTANCE.ExportStaticFuncB("ほげ"); System.out.println("ExportStaticFuncB ret=" + retStr ); //以下を追加 INSTANCE.CallStaticSecondFunc("ふが" ); INSTANCE.CallPublicSecondFunc(100 ); } } |
CallStaticSecondFuncとCallPublicSecondFuncの呼び出し部分を追加しました。
実行すると
と
というメッセージBOXが表示され、無事にJava->1段目のDLL->2段目のDLLと、順番に呼び出せたことがわかります。
参考にさせて頂いたサイト様など
●JNAについて
●DllExportについて
●その他
- WisdomSoft様:C++/CLIとは?
- Qiita様:.Net用DLLを動的に呼び出す:@kazuhiroxさん
- DoctorLabo様:DLL動的リンク
哀丁・四方山話一覧 へ戻る
(画像URL:illust-AC 様:wayo さん、acworks さん Free icons 様)