BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル 進行中の相互運用

進行中の相互運用

ブックマーク

あまり知られてはいませんが、非常によく使用されている2つのマネージド環境(JVMとCLR)は実際には、共有ライブラリセットにすぎません。それぞれが実行コードにメモリ管理やスレッド管理、コードコンパイル(JIT)などのサービスを提供しています。このため、同じオペレーティングシステムプロセス内でJVMとCLRの両方を用いることは大きな問題にはなりません。どのプロセスでも、ほぼすべての共有ライブラリをロードできるためです。

ただし、ここまで話を聞いたほとんどの開発者は手を止めて、首をかしげて、(至極当然なことながら)「でも、...なぜ」と疑問に思うはずです。

何年にもわたって、Javaのプラットフォームは、驚異的な数のAPIと技術を取り込んで成長しました(JavaSoundは、言うまでもありませんよね?)。もちろんCLRは、Windowsオペレーティングシステムの豊富な機能を理解した上で開発されたものです。しかし、Windows OSの豊富な機能のなかで現時点ではJavaでアクセスできないものがあり、こうした機能を直接使用するほうが簡単な場合があります。これは、方法に関わらず、Java内部でJNIを使用することに関してよくみられる議論であり、このこと自体はJava開発者にかなり知られているはずです。これに対して、新しい.NET3.0機能(主要なトピックである、WorkflowやWPF、InfoCard)などの.NET機能をJVM内部で使用することや、.NETプロセス内からJVMツールを使用する(Javaで記述された複雑なビジネスロジックを含んでいるSpring Beanのホスティング、あるいはASP.NET内部のJVMのキューへのアクセス)ことは、それほど知られていません。

DLLのロードとマネージド環境との通信は、別個のトピックですが、それぞれに実行用の標準APIが提供されています。たとえば、JNIドキュメントから以下の(アンマネージド)C++コードを入力することにより、標準(アンマネージド)プロセス1からJVMが起動されます(参考文献1を参照)。

#include "stdafx.h"
#include

int _tmain(int argc, _TCHAR* argv[])
{
JavaVM *jvm; /* denotes a Java VM */
JNIEnv *env; /* pointer to native method interface */
JavaVMInitArgs vm_args; /* JDK/JRE 6 VM initialization arguments */

JavaVMOption options[4]; int n = 0;
options[n++].optionString = "-Djava.class.path=.";

vm_args.version = JNI_VERSION_1_6;
vm_args.nOptions = n;
vm_args.options = options;
vm_args.ignoreUnrecognized = false;

/* load and initialize a Java VM, return a JNI interface
* pointer in env */

JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args); // cast necessary to make C++ happy

/* invoke the Main.test method using the JNI */
jclass cls = env->FindClass("Main");
jmethodID mid = env->GetStaticMethodID(cls, "test", "(I)V");
env->CallStaticVoidMethod(cls, mid, 100);

/* We are done. */
jvm->DestroyJavaVM();

return 0;
}

このコードをコンパイルするためには、JDKのincludeとinclude\win32のディレクトリがC++コンパイラのincludeパスの一部である必要があり、JDKのjvm.lib(JDKのlibディレクトリで見つけられる)がリンカーのパス上になければなりません。起動する場合、Main.classファイルは、実行する実行ファイルと同じディレクトリにあることと、ライブラリを共有しているJREの"jvm.dll"が、通常PATHにそれを加えると見つけられることが前提となっています。(大抵の場合、java.exeランチャーが動的に検索し、一度それを見つけると結合しますから、"jvm.dll"がPATHにある必要はありません。)

同様に、CLRには同じことを実行するために、Hosting APIと呼ばれる独自のAPIが提供されています。

#include "stdafx.h"
#include

int _tmain(int argc, _TCHAR* argv[])
{
ICLRRuntimeHost* pCLR = (ICLRRuntimeHost*)0;
HRESULT hr = CorBindToRuntimeEx(NULL, L"wks",
STARTUP_CONCURRENT_GC, CLSID_CLRRuntimeHost, IID_ICLRRuntimeHost,
(PVOID*)&pCLR);
if (FAILED(hr))
return -1;

hr = pCLR->Start();
if (FAILED(hr))
return -1;

DWORD retval = 0;
hr = pCLR->ExecuteInDefaultAppDomain(L"HelloWorld.exe", L"Hello", L"Main", NULL, &retval);
if (FAILED(hr))
return -1;

hr = pCLR->Stop();
if (FAILED(hr))
return -1;

return (int)retval;
}

JNIの例のように、この実例では、実行の際、現在のディレクトリにHelloWorld.exe .NETのアセンブリ2があると仮定されています。CLRは、オペレーティングシステムに非常に深い"フック"を持っているので、CLRの実現DLLはPATH上にある必要はありません(CLRブートストラッププロセスの動作方法の詳細については、『Shared Source Essentials』を参照ください)。

2つのランタイムをそれぞれロードし、その内のどちらかに処理権限を委ねるアプリケーションをアンマネージドC++で開発者が記述するのは確かに可能でしょうが、これは、ほとんどの開発者が今日踏みたがらない領域、非マネージドで昔風のC++で開発する領域にアプリケーション開発論理の大部分を堂々と持ち込むようなものです。もう一度このスキルを実行するかもしれないほど誘惑されたとしても、我々の多くには代替手段が多数あります。

たとえば、何よりもまず、両方の技術がアンマネージドコード(JVMの場合は、JNI、CLRの場合は、P/Invoke)、に"命令を下す"ことをサポートし、これによって、これらの間で少量の"トランポリン"コードを通して、他の言葉で利用されるために、ある環境で宣言されたメソッドを実装する機会が提供されます。たとえば、Javaでは、JNIで本来の方法を実行するのは比較的まれで、他の文献3にも記述されています。ここに加えられている新方式のものだけが、MicorsoftのC++/CLI(Visual Studio2005が提供)か、あるいは、マネージドC++(Visual Studio2003が提供)コンパイラを利用した本来のC++実装を実行することになるでしょう。

現時点では、ただ複雑であるがために、このDLLを実行時に検出する方法をJMVが知っていると確認することが重要になります。これは、2つの部分で実行されます。つまり、最初に、本来の方法を利用しているJavaクラスがロードされたときに、RunTime.loadLibrary()コールを通して、それをバックアップする共有ライブラリをロードすることを、JVMに要求する必要があります。要求される本来のライブラリは、ファイル名の拡張子が明記されずに、そのようにして実行されることに注意してください。この拡張子不足は、意図的です。つまり、さまざまなオペレーティングシステムが共有ライブラリに対していろいろな仕様を用いているので(Windowsの下では、共有ライブラリは、".DLL"が末尾についたものであるのに対して、Unixライクのオペレーティングシステムの下では、その仕様に"libNAME.so"と名前が付けられなければならない)、基本名だけが渡されるのです。この時点で、JVMは、ホストとして働くOSが利用するあらゆる仕様に沿った共有ライブラリを検索するはずです。Windowsの場合、このことは、LoadLibrary()用のAPI資料で正式に文書化されていますが、オペレーティングシステムのインストールディレクトリ(C:\WINDOWS and C:\WINDOWS\SYSTEM32)や現在作業中のディレクトリ、PATHを原則的に含みます。JVMは他に2つの場所を調べるはずです。ただし、"java.library.path"システムのプロパティに明記されたディレクトリに沿ってと、実行JREの下の"lib\i386"ディレクトリの中をですが。一般的に言えば、(JVM起動時に指定されたシステムプロパティに関して管理するのであれば)"java.library.path"プロパティで特定される個別の場所か、あるいは、実行JREの"i386"ディレクトリのどちらかで、本来の実現を続けることが推奨されます。この特殊な実例に対して、JVMのシステムプロパティを特定するのは、開発者の管理外であると仮定することが最も簡単で(JVMが数多くのアプリケーションサーバのためにあると思われる場合)、従って、DLLは、サーブレットコンテナ/アプリケーションサーバで利用されるJREの下にコピーされるべきです。DLLが一度見つけられれば、これが"混合モード"の.NET DLL(この内部にはマネージドコードとアンマネージドコードの両方があると言う意味)であるという事実によって、そのプロセス中にCLRの自動起動が余儀なくされるでしょうし、そしていま、CLRが全開となったことで、JNIのDLLが意のままになります。

これを180度反転すると、.NETアプリケーションは、もう1つのトランポリン(この場合もやはりアンマネージドDLL)を通してJVMに指令できます。ただし、このとき、JVMは、CLRが行うようないかなる種類のマジック起動を行わないので(WORAの価値)、アンマネージドDLLは、以前のように同じAPIを使って明示的にJVMをそのプロセス内にロードする必要があります。しかし、一度所定の位置に立ち上げられると、JNI反射のような呼び出しAPIによって、クラスのロードやオブジェクトの作成、メソッドの呼び出しが許可されます。CLRコードでアンマネージドDLLにアクセスすることは、単にP/Invoke APIを用いた作業のことであり、これについては、(この場合も同様に)他でこれまでにも多数書かれています。

このすべてが、膨大な量の作業のように思えるのなら、これを考えているのはあなただけではありません。都合のいいことに、このプロセスをとても簡単にする、利用可能ないくつかのツールと技術があります。

これらにはまず、Java JNIインタラクションを簡素化するオープンソースのツールキットがあり、JACE(http://jace.sourceforge.net/)と呼ばれ、ホストとしてJVMを処理することやJavaクラスのJavaメソッドを要求するなど、この両方とも簡単になるように設計されています。これによって、特にJVMが起動するときに、どちらかのシナリオのJNIの一部の書き込みがそれほど非常に簡素で簡単になります。他のストーリーは原形を保っていますが、ただし、JACEはアンマネージドC++利用が対象で、それ自体は、Windows固有のDLLに関していまだ逆戻りし、すべての種類を”危険な”コードで書いていることを示しています。

異なるラインに沿い、IKVMと呼ばれ、現在Monoプロジェクトの一部である別のオープンソースライブラリがあります。IKVMは、言及されている他のリソースのいくつかとは"相互運用性"に対して異なる手法を取っている点では異例で、JVMをロードするなどしてCLRとJVM4間の橋渡しすることはしません。IKVMでは、JavaのバイトコードをCILに変換するため、まったく同じプロセスにJMVをロードする必要はありません。JVMが決してロードされないためJVMの優秀さが何も発揮されないという点に関して、興味深いいくつかの言外の意味があります。つまり、ホットスポットが無く、監視のためのJMKフックもない(これは、Javaコードを監視するjconsoleの意味ではありません)、などです。もちろん、いまはすべてのコードがCILに変えられるので、それどころか、CLRのJITやCLRの性能モニターカウンタなどのCLRの優秀さが十分に発揮されています。また、IKVMは"オンザフライ"でバイトコード変換を実行できるので、その効果は、CLR開発者にとってかなりトランスペアレントです。

ただし、しばしば本当にJVMをロードしたいと思う場合があります。それは、CodemeshのJuggerNETユーティリティ5で生成されたプロキシなどの進行中のプロキシが助けに来るときです。これは、次の2つのことを規定しています。つまり、.NETアプリケーション内部でそれだけJVMを作りやすくする、.NETにとって使いやすいJNI呼び出しAPIのバージョンと、必要に応じてJava引数にする引数を操作する.NETプロキシを生成し、Javaオブジェクト上でJavaメソッドを実行するコード生成ユーティリティです。従って、.NETアプリケーションにJVMをロードすると以下のようになります。

 /*
* Copyright 1999-2006 by Codemesh, Inc. ALL RIGHTS RESERVED.
*/

using System;
using Codemesh.JuggerNET;

//
// This application programmatically configures a JVM and loads it.
//
// The JVM to be used is determined via platform-dependent logic
// in this example. You could also use the JvmPath property to
// programmatically configure the JVM to be used.
//
public class Application
{
public static void Main( string[] argv )
{
try
{
//--------------------------------------------------------------------
// the following line gives you access to an object you can use
// to initialize the runtime settings.
//

IJvmLoader loader = JvmLoader.GetJvmLoader();

//--------------------------------------------------------------------
// configure the Java settings
//


// set the classpath to the current working directory
loader.ClassPath = ".";

// append the CWD's parent directory to the classpath
loader.AppendToClassPath( ".." );

// set the maximum heapsize
loader.MaximumHeapSizeInMB = 256;

// set a couple of -D options
loader.DashDOption[ "myprop" ] = "myvalue";
loader.DashDOption[ "prop_without_value" ] = null;

// specify a trace file. If you don't, all tracing output will go to
// stderr

loader.TraceFile = ".\\trace.log";

//--------------------------------------------------------------------
// you can leave it at that and the configured settings will be used
// to kick off the JVM on-demand when the first proxy operation is
// executed OR you can explicitly load the JVM. If anything goes wrong
// an exception will be thrown.
//

loader.Load();

}
catch( System.Exception )
{
Console.WriteLine( "!!!!!!!!!!!!!!! we caught an exception !!!!!!!!!!!!!!!!" );
}

Console.WriteLine( "*************** we're leaving Main() ****************" );

return;
}
}

.NETからJavaへのプロキシコード生成は、少し手が込んでいますが、ただ、プロキシされるべきJavaクラスやパッケージを手動で指定する段階があるからで、これは、パッケージやリストに記載されている"モデルファイル"を指定するJuggerNET GUIツールか、あるいは、タスクである"

 /*
* Copyright 1999-2006 by Codemesh, Inc. ALL RIGHTS RESERVED.
*/


using System;
using Codemesh.JuggerNET;
using Java.Lang;
using Java.Util;

/// <summary>
///
A .NET type that declares data members.
/// By extending the <c>Serializable</c> proxy interface we
/// automatically gain the so-called "peer" capability for our
/// .NET type. The <c>Serializable</c> interface is marked in
/// the code generator as having a Java peer type that can hold
/// the serialized information of a .NET instance.
/// </summary>
public class MyDotNetClass : Java.Io.Serializable
{
public int field1 = 0;
public int field2 = 1;
public string strField = "<not set>";

public MyDotNetClass()
{
}

public MyDotNetClass( int f1, int f2, string s )
{
field1 = f1;
field2 = f2;
strField = s;
}

public override string ToString()
{
return "MyDotNetClass[field1=" + field1 + ", field2=" + field2 + ", strField='" + strField + "']";
}
}

/// <summary>
///
Another .NET type that extends <c>Serializable</c> but declares
/// different data elements.
/// </summary>
public class MyDotNetClass2 : Java.Io.Serializable
{
public int[] test = new int[] { 0, 1, 2 };

public MyDotNetClass2()
{
}

public MyDotNetClass2( int f1, int f2 )
{
test[ 0 ] = f1;
test[ 1 ] = f2;
}

public override string ToString()
{
System.Text.StringBuilder result = new System.Text.StringBuilder();

result.Append( "MyDotNetClass2[test=[" );
for (int i = 0; i < test.Length; i++)
{
if( i != 0 )
result.Append( "," );
result.Append( "" + test[i] );

}
result.Append( "]]" );

return result.ToString(); }
}

/// <summary>
///
This type illustrates how we can achieve the goal of peer serialization
/// by adding a <c>JavaPeer</c> attribute to the .NET type.
/// This creates similar usability to extending <c>Java.Io.Serializable</c>
/// but is inferior in one way: you cannot use an instance of a <c>PureDotNetType</c>
/// in a place that expects a <t>Serializable</t>.
/// The <c>JavaPeer</c> attribute here specifies two different properties:
/// <c>PeerType</c> and <c>PeerMarshaller</c>. The first property specifies the
/// Java type that will hold the data and the second property specifies the type of
/// the class that knows how to serialize a .NET instance into that Java intstance
/// and reverse.
/// </summary>
[JavaPeer(PeerType= "com.codemesh.peer.SerializablePeer",
PeerMarshaller= "Codemesh.JuggerNET.ReflectionPeerValueMarshaller")]
public class PureDotNetType
{
private char ch = 'a';
/// </summary>
///
A field setter which helps us illustrate that we actually read real
///information back from Java.
/// </summary>
public char CharProperty
{
set { ch = value; }
}

public override string ToString()
{
return "PureDotNetType[ch='" + ch + "']";
}
}

/// </summary>
/// This type illustrates the use of field attributes to control details
/// of the peer serialization.
/// </summary>
[JavaPeer(PeerType="com.codemesh.peer.SerializablePeer",
PeerMarshaller="Codemesh.JuggerNET.ReflectionPeerValueMarshaller")]
public class PureDotNetType2
{
///<summary>
///
This field will always have the value '42' after unmarshalling because
/// its value does not get serialized/deserialized.
/// </summary>
[NonSerialized]
public int NotUsed = 42;

/// <summary>
///
This field will always have the value null after unmarshalling because
/// its value does not get serialized/deserialized.
/// </summary>
[JavaPeer(Ignore=true)] public string AlsoNotUsed = null;

///<summary>
///
This field will get serialized/deserialized, but on the Java side
/// this field is known under the name 'CustomFieldName'. You usually
/// won't care about the Java name, but you might if a Java program might
/// gain access to the peer object and has to use its data.
/// </summary>
[JavaPeer(Name="CustomFieldName")]
public int OnlyUsedField = 2;

public override string ToString()
{
return "PureDotNetType2[NotUsed=" + NotUsed +
", AlsoNotUsed=" + ( AlsoNotUsed == null ? "null" : AlsoNotUsed ) +
", OnlyUsedField=" + OnlyUsedField + "]";
}
}

public class Peer
{ public static void Main( string[] args )
{
try
{
IJvmLoader loader = JvmLoader.GetJvmLoader();

if( args.Length > 1 && args[ 0 ].Equals( "-info") )
;//loader.PrintLdLibraryPathAndExit();

// create a hashtable instance

Java.Util.Hashtable ht = new Java.Util.Hashtable();

// create some pure .NET instances
object obj1 = new MyDotNetClass();
object obj2 = new MyDotNetClass2( 7, 9 );
PureDotNetType obj3 = new PureDotNetType();
PureDotNetType2 obj4 = new PureDotNetType2();

obj3.CharProperty = 'B';

// these two values will be lost after we get the object back from the hashtable
obj4.NotUsed = 511;
obj4.AlsoNotUsed = "test";
// this value will be retained but under a different name on the Java side
obj4.OnlyUsedField = 512;

// put the .NET instances into a Java hashtable
// please note that there is no original Java type available
// for these .NET types; under the hood, the .NET object state
// is copied into a generic Java instance

ht.Put( "obj1", obj1 );
ht.Put( "obj2", obj2 );
ht.Put( "obj3", obj3 );
ht.Put( "obj4", obj4 );

// this is the REAL test!
// now we try to get back the original .NET information.

object o1 = ht.Get( "obj1" );
Console.WriteLine( "o1={0}", o1.ToString());

object o2 = ht.Get( "obj2" );
Console.WriteLine( "o2={0}", o2.ToString());

object o3 = ht.Get( "obj3" );
Console.WriteLine( "o3={0}", o3.ToString());

object o4 = ht.Get( "obj4" );
Console.WriteLine( "o4={0}", o4.ToString());

Console.WriteLine( "ht={0}", ht.ToString() );
}
catch( JuggerNETFrameworkException jnfe )
{
Console.WriteLine( "Exception caught: {0}\n{1}\n{2}", jnfe.GetType().Name,
jnfe.Message, jnfe.StackTrace );
}
}
}

全体的に見て、inproc interopという実行可能な手法が議論のテーブルにも上がっていない理由は、あまりはっきりしないように思えます。明らかに速度が利点であること以外に(シングルプロセスでデータを移動するのは、たとえ速いギガビットでネットワークのあらゆるところにデータを移動するよりも相当に速いので)、下記に示すように他に多くの利点があります。

  • 集中化。多くの場合、プロセス間で複雑なコードの同期を取るのを避けるため、いくつかのリソース(たとえば、コード内で生成されるデータベースシーケンス識別名)が唯一のプロセス内に存在する必要があります。
  • 信頼性。単一マシンの機能停止に対しては、関係するハードウェア層が少なくなるほど、システム全体の脆弱性が低くなります。
  • アーキテクチャの要件。いくつかのシナリオでは、既存のアーキテクチャ要件で、すべての処理が特別なプロセス内で行われるように規定されています。たとえば、アプリケーションのための既存のユーザーインターフェイスは、すでにASP.NETでコード化され、アプリケーションの相互運用の役割は、EJBメッセージ駆動型Beanが処理するメッセージをJMSキューに入れることです。JMSキューにメッセージを単に入れるJavaサービスに処理以外のメッセージを送るのは、特にJMSクライアントコードが一般的に言って簡単であるならば、幾分冗長であり費用もかかります。JMSクライアントコードをASP.NETプロセス(Codemeshは、JMSクライアントシナリオのために明確に指定されたJuggerNETプロキシのバージョンを提示します)の内部に入れると、既存アーキテクチャの"流れ"と調和が保てる最も簡単な方法が提供されます。

この場合も、すべての相互運用ソリューションがin-procで実現されるとは限りませんが、いくつかは実現されるでしょうし、開発者は、利用可能なツールが豊富に与えられているのですから、このソリューションに対して消極的であってはいけません。

筆者について

Ted Neward氏は、大規模な企業システムを専門とするフリーランスのコンサルタントです。巡回会議で、Javaや.NET、XMLサービス技術を話題にし、Java‐.NET相互運用に重点を置いた講演を行っています。最近発売された『Effective Enterprise Java』を含め、Javaと.NETの両方に関する広く認められた本を数冊書いています。

リソース

  • 『The Java Native Interface』著者 Liang
  • 『Java Native Interface』著者 Gordon
  • Java SEサイト(http://java.sun.com/javase/6/docs/technotes/guides/jni/index.html)
  • 『Customizing the Common Language Runtime』著者 Pratschner
  • 『Shared Source CLI』著者 Stutz、 Neward、 Shilling
  • C++/CLI言語仕様(ECMA International)

1添付のコードバンドルには、JNIHostingのサブディレクトリに、InProcInteropソリューションの一部として、これがあります。これを構築する最もよい方法は、コマンドラインから、JAVA_HOME環境変数でJDK1.6のディレクトリの場所を指し示すことです。
2ここで利用しているのは特殊なHosting API(ExecuteInDefaultAppDomain)であるため、これは、その内部にHelloと呼ばれるクラスを持つと予想され、言い換えるとこれは、引数として1文字を取り、1つの整数を返すMainと名づけられたメソッドを持つ必要があります。これは、C#、あるいは、VB.NET用の従来の入口点とは異なるシグネチャであることに注意してください。
3Liang氏、あるいは、Godon氏などの本、またはJDKのJNIドキュメントをご参照ください。
4現在(そして、当面)、他の回避方法に戻らず、CLRからJVMに進むはのIKVMだけです。
5JuggerNETは、JunC++tionである別のプロキシツールの.NETバージョンで、JunC++tionは、Java-C++プロキシツールです。

原文はこちらです:http://www.infoq.com/articles/in-process-java-net-integration

(このArticleは2007年1月29日にリリースされました)

この記事に星をつける

おすすめ度
スタイル

BT