次の方法で共有


J/Direct とネイティブ コード

DR. GUI Online
パート 1 : 1997 年 11 月 17 日
パート 2 : 1997 年 11 月 24 日
パート 3 : 1997 年 12 月 1 日
パート 4 : 1997 年 12 月 8 日

Dr. GUI
MSDN オンライン

目次

パート 1 : Dr. GUI は J/Direct のネイティブになる
パート 2 : ネイティブになるには
パート 3 : 今回もネイティブ
パート 4 : 結論、まだコラムは終わらない

パート 1 : Dr. GUI は J/Direct のネイティブになる

またもや Java ?

Dr. GUI のコラムの読者であればよくご存知だろうが、Dr. GUI の Java に対する考えには浮き沈みがある。Dr. GUI も苦労せずにプラットフォーム間でコードを移植できたらと思う。これはプログラマならだれもが願うことだ。しかし、これまで Dr. GUI は、クロス プラットフォームにおける非互換性やバグ、ときにはかなり深刻な性能の問題などについて、たくさんのことを述べてきた。

クロス プラットフォームの API としては、Java は大げさに言われるほどのものではない。ポーランド人の祖母はよく言ったものだ。"一度ばかにされたら、相手を恥じよ。二度ばかにされたら、自分を恥じよ。" もちろん、Dr. GUI は二度とばかにされるつもりはない。

すばらしい言語 . . . 

Java は言語としてはすばらしい。Java にはオブジェクト指向のプログラミングに必要なツールが揃っているから、C++ 言語という複雑な "オブジェクト指向アセンブリ言語" を使わなくて済むようになる。Java が登場したころ、私は C++ 言語の対話型チュートリアルを作成するプロジェクトに携わっていた。C++ 言語よりも Java を教えるほうがはるかに簡単だろう。当時は Java が羨ましかったものだ。

Java 言語が多くのプログラマに好まれているのには、理由がある。C++ 言語よりも簡単であることに加え、ガベージ コレクションなどの便利な機能を備えているからだ。C++ 言語のプログラマにとって、メモリをいつ解放するか悩まなくてもよいのは大きな救いである。ガベージ コレクションは、作業の効率化に寄与する。

. . . 特に Win32 プログラミングにおいて

Win32 でプログラミングする場合、Java は有力な選択肢である。ただし、パフォーマンスが重要な場合は注意する必要がある。少なくとも 1 つの会社は Java の性能に失望し、Microsoft Visual Basic でプログラムを書き換えた。その結果、性能が 2 桁向上したそうである。

Java を超えてネイティブへ

"Java 言語はすばらしい。だが Java API には不足しているものがある" と聞いたことがあるだろう。だが、Java を実際に使う場合はどうしたらよいのだろうか。Java でサポートされていない Windows 機能にアクセスしなければならない場合は、どうすればよいのだろうか。

ご承知のとおり、どのような言語でも、オペレーティング システムから提供されるネイティブ機能を使用できる必要がある。Java は組み込み機能が不完全なので、特にそれが必要である。Java を作った人でさえこの必要性を感じたらしく、Java にはネイティブ メソッドを記述する方法が用意されている。

ただし、忘れてはならない。いったんネイティブ メソッドを使用したら、夢のクロス プラットフォーム対応には別れを告げなければならない。ネイティブ呼び出しを小さなクラス群の中に隠ぺいすれば、ほとんどのクラスを移植可能にできるかもしれないが、ネイティブ メソッドの使用は決してクロス プラットフォーム ソリューションの助けにはならない。ネイティブ メソッドは、顧客を満足させるために必要なスピードと機能を得るための重要な方法なのである。

Java 仮想マシン (VM) が提供するネイティブ メソッドのインターフェイスには、複数の形式がある。ネイティブ メソッドのインターフェイスは、Java で最も一貫性のない部分の 1 つである。Win32 の場合、直接ネイティブ メソッドを呼び出す手段が現在までに 4 つ作成された。そのうえ、Sun と Microsoft ではネイティブ メソッド インターフェイスの働き方がいつも異なっている。

Microsoft がサポートする方法は、RNI (Raw Native Interface) と呼ばれる。RMI (Remote Method Invocation) と混同しないでほしい。説明書を少し読んでみると、RNI は確かに "raw" (生) である。ポインタのロック、ガベージ コレクションを有効にするかどうかなど、多くのことを気にしなければならない。RNI から呼び出すダイナミック リンク ライブラリ (DLL) は、RNI 用の特別な形式で記述する必要がある。ラッパー DLL を用意することはできるが、それでも任意の DLL を呼び出せるようになるわけではない。

このように複雑ではあるが、RNI はすべてのオブジェクトの全フィールドに完全にアクセスすることができ、とても高速である。しかし、RNI は非常に複雑なので、VM の機能拡張のようなシステム レベルのプログラミング以外では使わない方がよいだろう。脳外科手術をしないのなら、触らない方がよい。

我々もネイティブ コードを使用できる

ありがたいことに、脳外科手術をしなくても、ネイティブ コードにアクセスする方法がある。Java/COM インテグレーションと J/Direct である。

Microsoft の Java VM では、COM オブジェクトを一種の Java クラスとして扱うことができる。Java/COM インテグレーションにより、すべての Java Bean は ActiveX コントロールになることができ、すべての ActiveX コントロールは Java Bean になることができる。ほとんど自動的にである。COM オブジェクトでラップされた API や ActiveX コントロールを呼び出す場合は、COM インテグレーションが最適である。今後、このコラムでは、Java から COM オブジェクトを使う方法や、Java で COM オブジェクトを記述する方法について述べる予定である。

この方法にも 1 つだけ欠点がある。Java と COM の間に変換レイヤが必要になるので、C++ 言語から COM を呼び出す場合に比べると、パフォーマンスが低くなるのである。

また、この統合は強力だが、Windows API などの C 形式の DLL を呼び出す場合は、COM オブジェクトをラッパーとして用意する必要がある。

直接呼び出す

そこで、J/Direct が登場する。J/Direct を使用すると、ほとんどの DLL を "ダイレクト" に呼び出すことができる。つまり、RNI や COM のラッパー DLL を記述する必要がないのである。データ型の対応付けなどの困難な問題は、VM が引き受ける。したがって、COM オブジェクトでラップされていない API を呼び出す場合は、J/Direct が最適である。J/Direct は、Windows システムの DLL も、独自に作成した DLL も呼び出せる。つまり、すべての Windows API にアクセスできる。Java で Windows プログラムを記述できるようになるのである。

それにはどうすればよいのか

特別なディレクティブをコメント内に記述し、直後に静的なネイティブ メソッドを宣言する。そして、宣言したメソッドを呼び出す。これだけである。パラメータの変換や呼び出しシーケンスの調整は VM が行うから、ただ呼び出すだけでよい。ほかの方法のように、ラッパーの DLL や COM オブジェクトを記述する必要もない。必要な関数を直接呼び出すだけだ。Java から MessageBox 関数を使用するコード例を次に挙げる。Java には MessageBox のように簡単なものがまだない (Dr. GUI には信じられないのだが)。

  class ShowMsgBox {
  public static void main(String
   args[])
  {
    MessageBox(0, "テスト成功",
        "このメッセージ ボックスは Java から呼び出されています", 0);
  }

   /** @dll.import("USER32") */
   private static native int
   MessageBox(int hwndOwner, String
     text, String title, int fuStyle);
}

このクラスには、関数の宣言 (MessageBox) と関数の定義 (main) がある。main 関数は Java アプリケーション (アプレットではない) の標準エントリ ポイントである。上のコードでは、この関数から Windows API の MessageBox を直接かつ簡単に呼び出している。

MessageBox の宣言をこれ以上簡単にすることはできないだろう。これは Java 標準のネイティブ メソッド宣言である。ただし、直前に "ディレクティブ" と呼ばれる特別なコメントを記述して、MessageBox のエントリ ポイントがどの DLL に含まれているかをコンパイラに教える必要がある。上の @dll.import ディレクティブは、MessageBox が USER32.DLL にあることを示している。

これで、main 内のコードだけでなく、すべてのクラスのすべてのメソッドから MessageBox を呼び出せる。main 外のコードから呼び出す場合は、ShowMsgBox.MessageBox とすればよい。まったく自然で簡単だ。

このように、J/Direct は Java プログラムから Windows にアクセスするための強力な機能である。ほとんどすべての Windows API が呼び出せる。API で使用されているデータ型が Java に変換できない場合は制限があるが、そのようなケースは少ない。ポインタとコールバックもサポートされている。

J/Direct を使用するコードはすべてを行えるから、安易に信頼してはならない。したがって、アプリケーションでは J/Direct を実行できるが、アプレットでは実行できないようになっている。ただし、J/Direct を呼び出すコードを Java のラッパー オブジェクト内に入れ、そのオブジェクトが安全であることを 1 つ以上のカテゴリにおいて署名すれば、アプレットでも J/Direct を使用できる。

@dll.import

@dll.import について理解しておく必要がある。このディレクティブは、どの DLL をリンクすればよいかを Java VM に通知する。

クラス宣言の直前にこのディレクティブを記述すると、クラス内のすべての静的なネイティブ メソッドに対して、DLL を指定できる。MessageBox の例のように、使用する DLL を各メソッドごとに指定することもできる。

また、@dll.import には、序数のリンク、関数名のエイリアス、および呼び出し規約の変更を可能にするパラメータがある。これについては来週説明しよう。

Windows API へのアクセスをさらに簡単にする

"確かにすばらしいが、すべての Windows 関数呼び出しにこのような変わったコメントを付けたくはない" と思う人もいるだろう。そこで、すべての Windows API を各プログラマが自分で宣言しなくても済むように、これらの宣言を行うクラス群が Microsoft から提供されている。後は正しいクラスをインポートするだけでよい。

これらのクラスは、どれも "com.ms.win32" パッケージに含まれている。上の MessageBox の例も次のように簡単になる。

  import com.ms.win32.*;

class ShowMsgBox {
  public static void main(String
   args[])
  {
    MessageBox(0, "テスト成功",
        "このメッセージ ボックスは Java から呼び出されています", 0);
  }
}

各 Windows DLL に対して 1 つずつクラスがある。各クラスでは、利用可能な API が宣言されている。

インポート DLL 名 内容
Kernel32.class KERNEL32.DLL (基本 API)
Gdi32.class GDI32.DLL (グラフィックス)
User32.class USER32.DLL (ユーザー インターフェイス)
Advapi32.class ADVAPI32.DLL (暗号関連の API、イベント ロギング)
Shell32.class SHELL32.DLL (Windows Explorer へのインターフェイス)
Winmm.class WINMM.DLL (マルチメディア)
Spoolss.class SPOOLSS.DLL (スプーリング)

たとえば、MessageBox は USER32.DLL に含まれているので、User32.class で宣言される。この User32.class をインポートすれば、直接 MessageBox を呼び出すことができる。すべてのパッケージをインポートしてもよい。

Windows で使用される定数は、"win" クラスで利用できる。これらにアクセスするには、com.ms.win32 をインポートし、下で示しているように "win.CONSTANT_NAME" と記述する。近道しようとして、自分のクラスを com.ms.win32.win の拡張として実装してはならない。そうすると、そのクラスをロードするときに win クラス全体がロードされることになる。とても遅くなるだろう。したがって、省略しないで "win.xxx" と記述する必要がある。

  System.out.println("MAX_PATH = " + win.MAX_PATH);

Windows 構造体は、構造体名と同じ名前のクラス ファイルで宣言される。たとえば、RECT.class などである。これらをインポートすると、Windows 構造体を Java クラスであるかのように使用できる。

もう一度確認する。com.ms.win32.* をインポートすると、これらのクラスのすべてにアクセスできる。これらのクラスは SDK (Software Development Kit) for Java 2.0 のインストールではオプション扱いなので、インストールされていない可能性がある。その場合はインストールし直せばよい。これらのクラスは宣言だけを含むので、ユーザーがそれらをマシンにインストールする必要はない。しかし、コンパイルを行う開発者には必要である。

実際に使ってみよう

J/Direct を始めるにあたって必要となる基本はこれでわかった。J/Direct を実際に使用するには、SDK for Java 2.0 が必要である。Microsoft Internet Explorer 4.0 もあった方がよい。

ただし、まだすべてのことを行えるわけではない。構造体とコールバック関数について説明していない。したがって、Petzold の GENERIC.C を Java で実装することはまだできないだろう。Microsoft の Java の Web サイト https://www.microsoft.com/java/default.htm で詳細が説明されているので、早く先に進みたい人は、そちらを参照していただきたい。パート 2 では、J/Direct について残りを説明する。

パート 2 : ネイティブになるには

パート 1 では、J/Direct を使って一般の DLL や Windows システム DLL を直接呼び出す基本的な方法について述べた。その話は終わりにして、これから J/Direct の巧妙で複雑な部分を見ていくことにする。

問題は、Java と Windows では物事の扱い方が少し異なるということだ。Windows は主に C または C++ で記述されている。データ型が異なる場合があり、メモリの制御方法も違う。Windows にあって Java にはない概念もある。たとえば、関数へポインタを使ったコールバックは、Java の世界には存在しない。また、OLE の関数には特別なルールがある。

これらの違いがあるため、J/Direct は本当はダイレクトではない。ダイレクトに見えるだけだ。すべての呼び出しは、どこかのレベルで VM によって解釈される必要がある (マーシャリング)。通常、DLL の呼び出しなら J/Direct はほかの方法よりも優れているが、呼び出しのたびに往復とも変換レイヤを通る必要があるので、C++ 言語ほど高速ではない。しかし、そもそも Java を使ってアプリケーションを作成するなら、パフォーマンスをそれほど重視してはならないことを既に了解しているだろう。

今回、Dr. GUI はデータのマーシャリングについて説明する。コールバックの使い方や DLL の動的なロード方法などについては、パート 3 以降で説明する。

データ マーシャリングを使ったデータ変換

Java のデータ型は C および C++ 言語のデータ型と似ているが、正確には対応していない。組み込みデータ型には自然な対応関係があるが、関数パラメータや構造体フィールドなどのデータ要素では、いくつか違いがある。

文字列、構造体、ポインタ、コールバック、およびポリモーフィックなパラメータは、特別に処理する必要がある。ポリモーフィックなパラメータというのは、状況に応じて異なるデータ型に解釈されるパラメータのことである。

次の表は、パラメータと戻り値について、データ型の対応関係を示している。

Java C、C++、ネイティブ 備考と制限
byte BYTE または CHAR
short SHORT または WORD
int INT、UINT、LONG、ULONG、または DWORD
char TCHAR
long _int64 これは注意が必要。Java の long は 64 ビットだが、C/C++ の long は 32 ビットである。
float float
double double
boolean BOOL
String LPCTSTR OLE モード以外では、戻り値として使用できない。OLE モードでは、String は LPWSTR に対応する。Microsoft VM は CoTaskMemFree を使って文字列を解放する。
StringBuffer LPTSTR 戻り値としては使用できない。DLL 関数が生成する最大の文字列を保持できる十分な StringBuffer 容量を設定する必要がある。
byte[] BYTE* または CHAR*
short[] SHORT* または WORD*
char[] TCHAR*
int[] INT*、UINT*、LONG*、ULONG*、または DWORD*
float[] float*
double[] double*
long[] __int64*
boolean[] BOOL*
オブジェクト 構造体へのポインタ OLE モードでは、代わりに IUnknown* が渡される。
インターフェイス COM インターフェイス JActivex または同等のツールを使ってインターフェイス ファイルを生成する。
com.ms.com.SafeArray SAFEARRAY* 戻り値としては使用できない。
com.ms.com._Guid GUID,IID,CLSID
com.ms.com.Variant VARIANT*
@dll.struct クラス 構造体へのポインタ
@com.struct クラス 構造体へのポインタ
void VOID 戻り値としてのみ使用できる。
com.ms.dll.Callback 関数ポインタ パラメータとしてのみ使用できる。

上のマッピングのほとんどは、思ったとおりだろう。byte は char に、int は int に、long は _int64 に、boolean は BOOL に対応している。だが、いくつか意外なマッピングもある。また、パラメータであるか、@dll.struct で宣言された構造体のメンバであるかによって異なるマッピングもある。@dll.struct についてはこの後で触れる。

Java には char 以外に符号なしのデータ型が存在しないため、符号なしの値を扱う場合は注意が必要だ。また、Java の long は 64 bit であり、C/C++ のように 32 bit ではないことも忘れてはならない。

パラメータとして渡された配列は、配列要素の型へのポインタに対応付けられる。たとえば、Java の byte [] は C/C++ の CHAR* に対応する。戻り値が Strings または StringBuffers の場合は、制限がある。

構造体の問題

Java と C/C++ の間で構造体をマッピングすることは一見簡単に思える。要するに、構造体はメンバ関数を持たないクラスではないのか。

しかし、実際には C/C++ と Java では構造体の格納方法が大きく違うため、特別な問題が存在する。C/C++ では、構造体は連続するメモリ ブロックに格納される。また、多くの場合は、アラインメントを調整するためにメンバ間にパディング バイトが挿入されている。たとえば、2 バイト以上のメンバは偶数アドレスに配置され、4 バイト以上のメンバは 4 で割り切れるアドレスに配置される。

Java では、クラス内のフィールドの格納形式は実装に依存する。フィールドの参照は、少なくとも最初の 1 回は名前によって行われる。この方式により、フィールドとメソッドを追加してクラスを更新しても、古いコードは引き続きそのクラスを使用できる。

Java VM は、ガベージ コレクションを行う際にオブジェクトを移動する。オブジェクトへの参照を DLL 内の関数に渡す場合は、これが問題になる。特別に、構造体を移動しないようにする必要がある。

この問題に対する J/Direct の解決策は単純だ。クラスを宣言するとき、@dll.struct ディレクティブという "ファンキーなコメント" を付け加えるのである。その結果、VM は構造体のメンバを連続したメモリに格納し、データを移動しないようになる。構造体のパック方法 (パディング方法) は指定できる。次は、そのような構造体の宣言例である。

  /** @dll.struct() */
class SYSTEMTIME {
  public short wYear;
  public short wMonth;
  public short wDayOfWeek;
  public short wDay;
  public short wHour;
  public short wMinute;
  public short wSecond;
  public short wMilliseconds;
}

次は、この SYSTEMTIME 構造体を使って DLL メソッドを呼び出す例である。

  class ShowStruct {
    /** @dll.import("KERNEL32") */
    static native void GetSystemTime(SYSTEMTIME pst);
    public static void main(String args[])
    {
      SYSTEMTIME systemtime = new SYSTEMTIME();
      GetSystemTime(systemtime);
      System.out.println("Year is " + systemtime.wYear);
      System.out.println("Month is " + systemtime.wMonth);
      // など
    }
}

これまで見てきたように、面倒なことはすべて VM が行ってくれる。

構造体の中に配列、文字列、構造体などのオブジェクトが含まれていると、特別な問題が発生する。Java では、配列、文字列、およびその他のオブジェクトは、ヒープに割り当てられる。オブジェクトである構造体メンバは、オブジェクトへの参照として格納されている。たとえば、Java のクラス内で 5 つの int を含む配列を宣言すると、そのクラスのオブジェクトには、5 つの int を含む配列そのものではなく、5 つの int を含む配列への参照が格納される。

これに対して、C/C++ で構造体の中に 5 つの int を含む配列を宣言した場合は、5 つの int を含む配列そのものが構造体に格納される。ポインタを格納したい場合は、メンバをポインタとして宣言すればよい。Java にはこのような柔軟性がない。それでも、Java から C/C++ の DLL に構造体を渡す場合は、C/C++ のやり方に従う必要がある。

構造体の中に構造体を埋め込む場合は、両方の構造体に対して @dll.struct ディレクティブを指定する必要がある。または、別の方法として、埋め込まれる構造体のフィールドを埋め込み先の構造体のフィールドとして直接記述する。埋め込まれた構造体を参照すると VM によるマーシャリングが発生するが、フィールドを直接記述すれば、その時間を節約できる。

配列または文字列を埋め込む場合は、もう 1 つのディレクティブである @dll.structmap を使用する。このディレクティブは、Java の参照に対応する配列のサイズと型を指定する。たとえば、Windows の LOGFONT 構造体は C/C++ で次のように定義できる。

     typedef struct {
     LONG lfHeight;
     LONG lfWidth;
     /* <このほかのフィールドは省略> */
     TCHAR lfFaceName[32];
   } LOGFONT;

この構造体を Java で表すには、ディレクティブを使って文字列のサイズを指定する必要がある。

     /** @dll.struct() */
   class LOGFONT {
     int lfHeight;
     int lfWidth;
     /* <このほかのフィールドは省略> */
     /** @dll.structmap([type=TCHAR[32]]) */
     String   lfFaceName;
   }

@dll.structmap ディレクティブには、固定長文字列のサイズを文字単位で指定する。null 終端文字の分もサイズに含める。

埋め込まれた配列を表す場合も同じディレクティブを使用するが、構文は少し異なる。次の C/C++ の構造体があるとする。

  struct EmbeddedArrays
{
  BYTE     b[4];
  __int64  l[4];
  doubl    d[4];
};

これを Java で記述すると、次のようになる。

  /** @dll.struct() */
class EmbeddedArrays
{
    /** @dll.structmap([type=FIXEDARRAY, size=4]) */
    byte b[];
    /** @dll.structmap([type=FIXEDARRAY, size=4]) */
    long l[];
    /** @dll.structmap([type=FIXEDARRAY, size=4]) */
    double d[];
}

構造体のパック方法は、@dll.struct の "pack" 修飾子を使って指定する。たとえば、/** @dll.struct(pack=4) */ のようになる。値には 1、2、4、または 8 を指定でき、デフォルトは 8 である。この修飾子の効果は、C/C++ の #pragma pack(4) と同じである。

文字列

一般に、文字列はそのまま DLL 関数に渡すことができる。VM が自動的に必要な変換を行う。次に例を挙げる。

  class ShowCopyFile{
   public static void main(String args[]) {
      CopyFile("old.txt", "new.txt", true); }
   /** @dll.import("KERNEL32") */
   private native static boolean CopyFile(String existingFile,
      String newFile, boolean f);
}

Java の文字列は、読み取り専用である。そのため、Microsoft VM が String オブジェクトを受け付けるのは、受け取った文字列を変更しない DLL 関数に対してだけである。文字列を変更する DLL 関数には、代わりに StringBuffer オブジェクトを渡す必要がある。この場合は、StringBuffer に十分な容量を確保しなければならない (s = new StringBuffer(容量);)。次はその例である。

  class ShowGetTempPath{
   static final int MAX_PATH = 260;
   public static void main(String args[]) {
      StringBuffer temppath = new StringBuffer(MAX_PATH);
      GetTempPath(temppath.capacity()+1, temppath);
      System.out.println("Temppath = " + temppath);
   }
   /** @dll.import("KERNEL32") */
   private static native int GetTempPath(int sizeofbuffer,
      StringBuffer buffer);
}

StringBuffer のデフォルト コンストラクタを使用してはならない。デフォルト コンストラクタでは、16 文字分の容量しか割り当てられない。常に適切な StringBuffer が渡されるように、ラッパー関数を記述することもできる。たとえば、次のような関数を用意する。

  public static String GetTempPath()
{
  StringBuffer temppath = new StringBuffer(MAX_PATH-1);
  int res = GetTempPath(MAX_PATH, temppath);
  if (res == 0 || res > MAX_PATH) {
    throw new RuntimeException("GetTempPath error!");
  }
  return temppath.toString(); // StringBuffer を返すことはできない
}

この方法は便利で安全である。関数のエラー戻り値は、Java の例外にマッピングすればよい。StringBuffer オブジェクトを返すことはできないので、注意していただきたい。

@dll.import ディレクティブに "Unicode" 修飾子または "OLE" 修飾子を指定すると、文字列は Unicode 形式で渡される。どちらの修飾子も指定しないと、文字列は ANSI に変換される。たとえば、Windows NT でコードを実行する場合は、次のようにして、Unicode から ANSI、ANSI から Unicode への変換を避けることができる。

  /** @dll.import("KERNEL32.DLL, unicode) */

代わりに、次のように記述してもよい。

  /** @dll.import("KERNEL32.DLL, auto) */

後者のように記述すると、実行中の環境が Windows 95 と Windows NT のどちらであるかに応じて、VM が高速な方法を選択する。Windows API 関数を呼び出す場合は、常に "auto" を指定することをお勧めする。

選択された変換方法に基づいて、J/Direct は API 関数の適切なバリエーションを呼び出す。一部の API 関数には、MessageBoxAMessageBoxW のように、"...A" と "...W" の 2 つのバリエーションがある。最初に、"A" や "W" を追加しない関数名を使って呼び出そうとし、失敗すると、適切な文字を追加してもう一度呼び出す。

"OLE" モード以外で文字列を DLL 関数の戻り値の型として宣言することはできない。"OLE" モードでは、CoTaskMemAlloc 関数によって割り当てられた LPWSTR が戻り値として使用される。

次は何 ?

少し範囲を広げすぎただろうか。しかし、まだ終わりではない。J/Direct には、ほかにも魅力的な部分がある。ポインタ、コールバック、DLL の動的なロード、ポリモーフィックなパラメータ、OLE 関数の呼び出し、エラー関連情報についても説明したい。

今まで Java のプログラムを作ったことがなく、J/Direct を使ったことがなくても、ラテン語を学ぶことによって母国語のしくみが理解できるように、このシリーズによって C/C++ や Windows API に対する理解が深まることを Dr. GUI は期待している。

パート 3 : 今回もネイティブ

また皆さんにお会いできて嬉しい。Java 用 Microsoft 仮想マシン (VM) の 7 不思議の 1 つ、J/Direct をめぐる旅もあと 2 回となった。

パート 1 では、J/Direct の基本について説明した。パート 2 では、J/Direct による構造体や文字列など一般的なデータのマーシャリング方法について述べた。今回はもう少し難しい話題に挑むことにする。ポインタ、ポリモーフィックなパラメータ、およびコールバックである。最終回は今までのおさらいと、DLL の動的なロード、OLE 関数の呼び出し、エラー情報の取得、および J/Direct と RNI の比較について述べるつもりである。

ポインタ

Java の初心者でも知っているように、Java はポインタというデータ型をサポートしていない。その Java から、ポインタを多用する Windows を一体どうやって呼び出したらいいのか、Dr. GUI は考え込んでしまう。

答えは、目的によって異なる。単一のデータ オブジェクトへのポインタが必要な場合は、要素を 1 つだけ含む適切な型の配列を渡すことができる。その他の場合は、ポインタを Java の整数に格納する。必要ならば、ptrToStructptrToString などの DllLib メソッドを使用することにより、その整数 (本当はポインタ) を使って必要なデータにアクセスできる。

単純なポインタ

int 型へのポインタを受け取り、そこに値を格納する関数を呼び出す場合を考えてみよう。Java には、参照による受け渡し (参照渡し) を行う方法が 1 つだけある。配列を渡すのである。これを利用して、単一オブジェクトへのポインタの代わりに、1 要素の配列を渡すことができる。この例では、int 型の配列を渡すことになる。

たとえば、次の DLL 関数について考える。

  void foo(int * piStatus); // C/C++ 関数 : piStatus は 1 つの int へのポインタ

この DLL 関数は、次の Java 関数に対応する。

  /* J/Direct を使った Java */
/** @dll.import("foo") */
static native void foo(int [] piStatus);

したがって、次のように呼び出すことができる。

  /* J/Direct を使った Java */
int piStatus[1];
piStatus[0] = inValue;
foo(piStatus);
   int outValue = piStatus[0];

C/C++ の世界からのポインタ : ダム ハンドルとしてのポインタ

これで追加の戻り値を受け取るためのポインタの使い方は理解できたと思う。しかし、Windows DLL などの DLL がポインタを返してきた場合はどうだろうか。返されたポインタをどう処理すればよいのだろうか ?

DLL からポインタを渡された場合は、いくつかの選択肢がある。ポインタの操作や間接参照が必要ない場合は、int 型で格納できる。その int 値は、必要なときに DLL に戻すことができる。ポインタというよりハンドルやクッキーを扱っているような感じである。次の DLL 関数について考える。

  /* C/C++ 関数*/
foo * RetPtr();
void TakePtr(foo *inPtr);

この DLL 関数は、次の Java 関数に対応する。

  /* J/Direct を使った Java */
/** @dll.import("foo") */
static native int RetPtr();
/** @dll.import("foo") */
static native void TakePtr(int inPtr);

したがって、次のように呼び出すことができる。

  /* J/Direct を使った Java */
int thePtr = RetPtr();
TakePtr(thePtr);

ポインタをハンドルかクッキーのように扱っている。値をまったく使用せずに DLL に戻している。ただし、通常の Java データはガベージ コレクションによってメモリ内で移動することがあるので注意が必要だ。衛生局長官によると、通常の Java データ構造のアドレスを取得して使用するのは、プログラムの健康を害する可能性があるとのことである。

データへのポインタ

では、ポインタの指し示すデータにアクセスする必要がある場合は、どうしたらよいのだろうか。

ポインタが構造体を指している場合は、ポインタから構造体への参照を作成できる。作成された参照は、通常どおりに使用できる。構造体は @dll.struct で宣言する必要がある。

  /* J/Direct を使った Java コード */
/* com.ms.win32.RECT.class からの RECT 宣言 */
/* import com.ms.win32.*; */
RECT rect = (RECT)DllLib.ptrToStruct(RECT.class, rawPtr);
rect.left = 0;

文字列へのポインタを Java の String オブジェクトに変換するために、PtrToString、ptrToStringAnsiptrToStringUni などの DllLib メソッドを使用する。

また、オーバーロードされた DllLib.copy メソッドを使用して、生のポインタ (raw pointer) で示されたメモリの内容を Java のさまざまな型の配列にコピーすることもできる。

さらに、熟練した技術者やシステム プログラマであれば、グローバル メモリやタスク メモリの割り当ておよび解放、データ構造のアドレスの取得、各種の DllLib メソッドを使った指定メモリ アドレスへのバイトの読み書きを行うことも可能である。適切なセキュリティ権限がないと J/Direct のコードを実行できないように制限することもできる。

ポリモーフィックなパラメータ

"ポリモーフィックなパラメータとはいったい何か。Windows はいつから継承をサポートするようになったのか" という疑問の声が Dr. GUI には聞こえる。

確かに、聞きなれない名前である。Windows のパラメータには、同時に渡された別のパラメータの値に応じて、異なる型として解釈されるものがある。これは知っていた人もいるだろう。たとえば、Windows API の最後の DWORD パラメータには、直前のパラメータの値に応じて、文字列へのポインタ、MULTIKEYHELP 構造体へのポインタ、HELPWININFO へのポインタ、または普通の整数として解釈されるものがある。

どうすればよいだろうか。ポインタを int 型として渡すのだろうか。

もっと簡単である。可能性のあるパラメータ型のそれぞれに対して、関数のオーバーロードを個別に用意すればよい。関数を呼び出す場合、渡したパラメータの型に従って、Java が正しいオーバーロードを選択する。そして、J/Direct がパラメータを正しく変換する。これ以上簡単なものはないだろう。

戻り値の型がポリモーフィックである場合は、各オーバーロードに対して異なる名前のメソッドを用意する必要がある。Java でも C++ でも、戻り値の型の違いに基づいてオーバーロードを定義することはできない。これらの異なる名前のメソッドは、@dll.import ディレクティブの entrypoint 修飾子を使用して、同じ DLL 関数に対応付けることができる。@dll.import ディレクティブについては後で述べる。

エイリアス

Java のメソッドに DLL 関数とは異なる名前を付けるには、@dll.import ディレクティブに entrypoint 修飾子を追加し、等号 (=) の後に DLL 関数の名前を指定する。次はその例である。

  /** @dll.import("USER32", entrypoint="GetSysColor") */
static native int getSysColor(int nIndex); // 小文字の "g" であることに注意

次の例では、この機能を使ってポリモーフィックな戻り値を処理している。

  /** @dll.import("foo", entrypoint="Func") */
static native int intFunc();
/** @dll.import("foo", entrypoint="Func") */
static native long longFunc();
// Java での名前は異なるが、DLL での名前は同じである

序数でリンクするには、エントリ ポイントの名前を #<num> にする。<num> には序数が入る。次はその例である。

  /** @dll.import("foo", entrypoint="#24") */
static native int meanOldNum24(); // 序数でリンクする

コールバック

Windows のプログラミングでは、コールバック関数を使わなければ仕事が進まない。実際、WNDCLASS 構造体のメンバの 1 つはコールバック関数 (ウィンドウ プロシージャ : WndProc) へのポインタなので、ウィンドウ クラスを作成することもできない。ほかにも、ダイアログ ボックス、ウィンドウの列挙など、さまざまな機能がコールバックを使って実現されている。したがって、コールバックを処理できなければ、J/Direct は便利とは言えない。Petzold の GENERIC.C を Java に変換できなかったら、何の意味があるだろう。

メソッドへのポインタはもちろんのこと、ポインタをまったく持たない言語を使用して、どのようにコールバックを処理するのだろうか。それには、ちょっとしたトリックが必要になる。

まず、コールバック関数 (メソッド) を専用のクラスの中に置く。このクラスは com.ms.dll.Callback の派生クラスでなければならない。次に、int 型のパラメータをいくつか受け取り、void または int を返す "callback" という名前の関数をオーバーライドする。必要ならば、各種の DllLib メソッドを使ってパラメータを変換できる。なお、各コールバックに対して新しいコールバック オブジェクトを作成することになるので、多くのコールバックのように、コールバックが必要とするデータを余分なパラメータとして渡す必要はない。代わりに、パラメータをコールバック オブジェクト内に入れて渡せばよいのである。

たとえば、次のようにして EnumWindows を呼び出すことができる。

  class EnumWindowsProc extends Callback {
    public boolean callback(int hwnd, int lparam) {
      StringBuffer text = new StringBuffer(50);
      GetWindowText(hwnd, text, text.capacity()+1);

      if (text.length() != 0) {
        System.out.println("hwnd = " +
                           Integer.toHexString(hwnd) +
                           "h: Text = " + text);
      }
      return true;  // 列挙を続行するために、TRUE を返す
    }

  /** @dll.import("USER32") */
  private static native int GetWindowText(int hwnd,
                                   StringBuffer text, int cch);
}

EnumWindows を呼び出すには、このコールバックを次のように使用する。

    boolean result = EnumWindows(new EnumWindowsProc(), 0);

この関数呼び出しが終了するまで、コールバック オブジェクトのガベージ コレクションが行われないようにする必要がある。コールバックが EnumWindows などの 1 つの関数でしか使用されない場合は問題ない。J/Direct 関数の中にいるときはガベージ コレクションが行われないため、コールバック オブジェクトを直接使用すればよい。

しかし、タイマー コールバックのように、コールバックを渡した関数が終了した後で、そのコールバックを使用する場合がある。また、WndProc コールバックを WNDCLASS に格納するなど、ネイティブ データ構造にコールバック オブジェクトを格納する場合もある。これらの場合は、コールバック オブジェクトがガベージ コレクションによって回収されないようにする必要がある。それには、コールバック オブジェクトをルート ハンドル (com.ms.dll.Root) オブジェクトでラップする。

複数の関数呼び出しにわたってコールバックを格納しておくのは、次のようにとても簡単である。

  EnumWindowsProc MyCallback = new EnumWindowsProc;
int rootHandle = Root.alloc(MyCallback);
// 複数の関数呼び出しでコールバックを使用する
Root.free(rootHandle);

callback メソッドのアドレスをネイティブ構造体に格納するには、まず、ルート ハンドルでコールバック オブジェクトをラップする。次に DllLib.addrOf を使用してルート オブジェクトのアドレスを取得し、そのアドレスを構造体に格納する。たとえば、ウィンドウ プロシージャをウィンドウ クラス構造体の中に設定するには、WNDPROC クラスを記述して callback メソッドをオーバーライドする。

  class WNDPROC extends Callback {
   public int callback(int hwnd, int msg, int wparam, int lparam)
   { ... }
}

そして、次のように記述する。

  WNDCLASS wc = new WNDCLASS(); // com.ms.win32.WNDCLASS.class
int callbackroot = Root.alloc(new WNDPROC()); // コールバック
wc.lpfnWndProc = DllLib.addrOf(callbackroot);

次は何?

これで J/Direct の医療かばんにメスとピンセットが少し追加されたわけだ。しかし、完全な手術を行うにはまだ少し道具が足りない。パート 4 では、このシリーズのまとめとして、DLL の動的なロード、OLE 関数の呼び出し、エラー情報の取得について説明し、J/Direct と RNI を比較してみようと思う。

パート 4 : 結論、まだコラムは終わらない

数か月前に、"J/Direct についてコラムを書くなら、4 回くらい書く必要があるだろう" と言われても、まったく信じなかったと思う。おそらく、Dr. GUI はその人をやり込めていた。ありそうなことだ。でも、機嫌が悪いときだけだ。

しかし、思っていたよりずっと時間がかかった。Dr. GUI にはたくさんのフィードバックが送られてきた。このコラムはおもしろくて役に立つという声が多かった。そこで、もう 1 回続けようと思う。確かに 4 回になった。

パート 1 では、J/Direct の基本について述べた。パート 2 では、構造体や文字列など J/Direct の一般的なデータのマーシャリングを分析した。パート 3 では、少し難しい分野に取り組んだ。ポインタ、ポリモーフィックなパラメータ、およびコールバックだ。パート 4 では、OLE 関数の呼び出し、DLL の動的なロード、エラー情報の取得、J/Direct と RNI の違いについて説明し、総まとめを行うことにする。

それでは、騒ぎを起こさずに平穏に始めよう。

OLE API

OLE API の世界は関数呼び出しに新たな一連の規約をもたらす。

エラー状態の情報を得ることは重要だ。とりわけリモード サーバーの設定では重要となる。そのため、OLE32.DLL および OLEAUT32.DLL に入っている OLE 関数は、ほとんど常に関数が成功したかしないかを伝える HRESULT を返す。1 つしかない戻り値がエラー状態の通知に使用されるため、"本当の" 戻り値は、規約により、参照で渡された最後のパラメータを使って返される。このことが Java では問題になる。Java で参照渡しを実現するのは難しい。それには、要素を 1 つだけ含む配列を使用する必要がある。

また、OLE ではすべての文字列が Unicode 文字エンコーディングで表される。さらに、OLE 関数は CoTaskMemAlloc を使って割り当てた文字列を返す。これは、呼び出し側が CoTaskMemFree を使って文字列を解放することを想定している。J/Direct は、これらすべてに対処して、できるだけ簡単に OLE API を呼び出せるようにする必要がある。

比較するため、Win32 形式でコーディングした簡単な Add 関数を次に示す。

    int sum;
  sum = Add(10, 20);

この Add 関数は、OLE 形式では次のように記述される。

    HRESULT hr;
  int sum;
  hr = Add(10, 20, &sum);
  if (FAILED(hr)) {
    ...エラー処理...
  }

@dll.import ディレクティブで "OLE" 修飾子を指定すると、J/Direct がこれらの違いを吸収し、Java にとって自然な形にする。たとえば、Java 関数は "本当の" 戻り値を返す。J/Direct は HRESULT のチェックも代行し、HRESULT がエラーを示している場合は、ComFailException をスローする。したがって、プログラマは HRESULT をチェックする代わりに、例外を処理するだけでよい。

J/Direct を使った Java では、上の OLE の例は次のように宣言される。

  /** dll.import("OLELIKEMATHDLL", ole) */
private native static int Add(int x, int y);
// OLE における HRESULT Add(int x, int y, int * sum); の役割を果たす関数

そして、次のように呼び出される。

  int sum = Add(10, 20);
// Add が成功した場合は、ここに到達する。失敗した場合は、例外が発生する。

ただし、HRESULT が S_OK 以外の成功値 (S_FALSE など) であれば、J/Direct は ComSuccessException をスローしない。これは Java/COM インテグレーションの機能とは異なっている。S_FALSE などの成功値を検出するには、"OLE" 修飾子を使わないで、通常の DLL 関数として OLE 関数を呼び出す必要がある。

J/Direct では、文字列処理の違いも取り扱う。OLE 関数の間では、文字列は LPCOLESTR として渡される。OLE 関数から得た文字列を Java に返す場合は、まず、VM が初期化されていない LPCOLESTR へのポインタを OLE 関数に渡す。次に、VM は返された LPCOLESTR を Java の文字列に変換した後、LPCOLESTR を解放する。

GUID と VARIANT は、com.ms.com._Guid と com.ms.com.Variant クラスを使って渡される (_Guid のアンダースコアに注意)。メモリの割り当てが発生しないということ以外は、文字列の場合と同様である。

たとえば、OLE32 では、CLSID と、Microsoft Visual Basic の CreateObject 関数で使われる可読名とを対応付けるために、CLSIDFromProgIDProgIDFromCLSID の 2 つの関数が公開されている。そのプロトタイプは次のとおりである。

  HRESULT CLSIDFromProgID(LPCOLESTR szProgID, LPCLSID pclsid);
HRESULT ProgIDFromCLSID(REFCLSID clsid, LPOLESTR *lpszProgId);

Java では、これらに対応するメソッドを次のように宣言する。

    import com.ms.com._Guid;
  class OLE {
    /** @dll.import("OLE32", ole) */
    public static native _Guid CLSIDFromProgID(String szProgID);

    /** @dll.import("OLE32", ole) */
    public static native String ProgIDFromCLSID(_Guid clsid);
  }

COM インターフェイスへのポインタを渡すには、jactivex.exe などのツールを使って Java/COM インターフェイス クラスを生成する必要がある。このインターフェイス型のパラメータを宣言して、COM インターフェイスを受け渡すのである。

たとえば、システム クラス com.ms.com.IStream は、構造化記憶で使用される IStream* インターフェイスを表す Java/COM インターフェイスである。これを使用して、OLE32 の関数 CreateStreamOnHGlobal を次のように宣言できる。

  import com.ms.com.*;
/** @dll.import("OLE32", ole) */
public static native IStream CreateStreamOnHGlobal(int hGlobal,
                                     boolean fDeleteOnRelease);

実行時のロードとリンク

Windows には、ロード時ではなく実行時に DLL を読み込むように指定できる。言い換えると、名前を指定して任意の DLL を読み込むように Windows に要求できる。コンパイル時に DLL を決定できないことがあるため、これは便利だ。実行時に DLL の名前を作成する場合もある。たとえば、Windows 95 と Windows NT の間で、または似たようなデバイス ファミリの間で、DLL の名前が異なっている場合があるからだ。

Windows は LoadLibraryGetProcAddress でこれをサポートする。これらの関数は J/Direct でも使用でき、DLL を読み込んだり、呼び出したい関数を検索することができる。

ただし、関数の呼び出しには 1 つ問題がある。Java はメソッドの参照をサポートしていない。したがって、関数ポインタを int 型の変数に格納する必要がある。その関数を呼び出すには、msjava.dll が提供する呼び出しメソッドを使用する。呼び出しメソッドは、最初のパラメータとして関数のアドレスを含む int 値を受け取る。2 番目以降のパラメータは、そのまま呼び出す関数に転送される。

たとえば、C 言語で次のように記述されるプロトタイプを持つ関数があるとする。

  BOOL AFunction(LPCSTR argument);

Java では、Win 32 API 関数を宣言した後で、次のように記述する。

  /** @dll.import("msjava") */
static native boolean call(int funcptr, String argument);

int hmod = LoadLibrary("...");
int funcaddr = GetProcAddress(hmod, "AFunction");
boolean result = call(funcaddr, "Hello");
FreeLibrary(hmod);

これ以上簡単にはできないだろう。

エラーの発見

通常 Windows では、直前に呼び出した API によってセットされたエラー コードを取得するために、Windows API の GetLastError を使用する。しかし、Microsoft Win32 VM for Java は Java コードを実行する過程で独自に API 関数を呼び出すため、エラー コードは取得する前に上書きされてしまう。このため、J/Direct コードで直接 GetLastError を呼び出すことはできない。

DLL 関数によってセットされたエラー コードを確実に取得するには、2 つの作業が必要である。まず、SetLastError 修飾子を使用して、メソッドを呼び出した後ですぐにエラー コードを取得しておくように Microsoft VM に指示する。パフォーマンスを考慮して、これはデフォルトでは行われない。次に、com.ms.dll.DllLib.getLastWin32Error メソッドを呼び出してエラー コードを取得する。各 Java スレッドは、エラー コードを別々の領域に保持している。

たとえば、FindNextFile 関数はエラー コードによってステータス情報を返す。FindNextFile は次のように宣言できる。

  /** @dll.import("KERNEL32",SetLastError) */
static native boolean FindNextFile(int hFindFile,
    WIN32_FIND_DATA wfd);

典型的な呼び出しは、次のようになる。

  import com.ms.dll.DllLib;
boolean f = FindNextFile(hFindFile, wfd);
if (!f) {
  int errorcode = DllLib.getLastWin32Error();
}

J/Direct と RNI

J/Direct と RNI (Raw Native Interface) は、ネイティブ コードを呼び出すための相互補完的な技術である。なお、3 番目の技術として、Java/COM インテグレーションを使った COM オブジェクトの呼び出しもある。

RNI を使用するには、DLL 関数が厳密な名前付け規約に従い、Java のガベージ コレクタと連係する必要がある。RNI 関数は、時間のかかるコード、制御を譲渡するコード、ほかのスレッドを待機してブロックするコード、ユーザーの入力を待機してブロックするコードなどの前後で、GCEnableGCDisable を必ず呼び出す必要がある。RNI 関数は、Java 環境で動作するように特別に設計する必要がある。その見返りとして、RNI 関数は Java オブジェクト内部、および Java クラス ローダーに迅速にアクセスできる。RNI をうまく使用すれば、Java VM と API を拡張できる。

これに対して、J/Direct は既存のコードを Java にリンクする。J/Direct なら、Win 32 API 関数や、C/C++ または Visual Basic から呼び出すための DLL なども呼び出すことができる。これらの DLL は、Java のガベージ コレクタや、そのほかの巧妙な Java ランタイム環境に合わせて設計されたものではない。しかし、J/Direct が自動的に GCEnable を呼び出すので、ブロックする関数やユーザー インターフェイス関数を呼び出しても、ガベージ コレクタに有害な影響を及ぼすことがない。さらに J/Direct は、文字列、構造体などの一般的なデータ型を自動的に変換し、C の関数が受け取れる形式にする。それらの処理をコードに挿入したり、ラッパー DLL を記述する必要はないのである。

その代わり、J/Direct を使って呼び出した DLL 関数から、Java オブジェクトのフィールドやメソッドに自由にアクセスすることはできない。今回のリリースでは、@dll.struct ディレクティブを使って宣言されたオブジェクトのフィールドとメソッドだけがアクセス可能である。また、J/Direct を使って呼び出された DLL 関数からは、RNI 関数も呼び出すことができない。その理由は、ガベージ コレクタが DLL 関数と同時に動作すると、RNI から返されたオブジェクト ハンドルや DLL 関数によって処理されたオブジェクト ハンドルが不正な値になる可能性があるという点にある。

"100% Pure" Java にこだわらなければ、RNI、J/Direct、Java/COM インテグレーション、およびそれらの組み合わせにより、強力な機能を使用できる。Microsoft Win 32 VM for Java とコンパイラは、同一クラス内で J/Direct と RNI を自由に混在させることも許している。jactivex ツールを使用すれば、ほとんどの COM クラスや ActiveX コントロールをラップする Java クラスを簡単に生成できる。

やっと終わりだ !

ここまで読んでくれたことに感謝する。皆さんも私のように楽しみながら理解を深めてくれたことを期待している。そして、SDK for Java 2.0 の「About J/Direct」の著者たちにも感謝する。Web サイト「Microsoft Technologies for Java」(https://www.microsoft.com/java/) にコードや情報が掲載されているので、こちらも参照していただきたい。