65.9K
CodeProject 正在变化。 阅读更多。
Home

.NET 平台调用范式在 Java 中 (J/Invoke)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.20/5 (7投票s)

2007 年 7 月 24 日

CPOL

9分钟阅读

viewsIcon

36197

downloadIcon

585

介绍了一个 SDK,该 SDK 支持直接 Java 调用以导出常规 DLL 模块中的函数

引言

Java Native Interface (JNI) 用于编写需要处理无法完全用 Java 编程语言编写的情况的原生方法。主要地,Java 开发者需要调用 DLL 中的一些原生函数。为此,他应该编写一个带有原生方法的 Java 类,并编写一个 DLL 来实现这些带有导出函数的函数。在需要的地方,可以调用原生函数。

任何写过 JNI 代码的人都知道,使用 JNI SDK 需要付出巨大的努力。Java 和原生非原始数据类型需要用 JNI 函数进行封送和解封送:Unicode 和 ANSI 字符串、Java 数组等。考虑到大多数 Java 开发者不了解 JNI SDK。当我们用 Microsoft VB 编写代码时,调用任何原生函数都没有问题。只需写出其原型,其中包含有关其所属库及其原始名称的数据布局。Microsoft 在 .NET 语言中使用的相同范式称为 .NET Platform Invoke。其主要思想是“声明”和“使用”一个原生函数。

问题现状

我们不是第一个试图用某种方式替代 JNI 以便在 Java 代码中轻松使用原生库的人。在 Web 上有一些产品,其作者声称可以在 Java 代码中调用原生函数而不使用 JNI。这些是 xFunction、J2Native、JNIWrapper、Neva 等。然而,这些产品并未解决问题。它们使用间接函数调用的思想——类似于反射——带有数据封送描述,包括原始类型。当一个函数将结构作为参数时,您将编写复杂的代码来描述结构的封送和解封送、字段对齐、联合描述等。有时代码变得如此复杂,以至于对我来说,最好编写一个简单的 JNI 模块,而不是大量不清晰的 Java 代码。

Java 虚拟机可以调用绑定到 Java 类原生方法的导出函数。在调用堆栈中,原始数据类型的原始值已经封送。因此,JVM 做了一些工作,如果实现了,其余的则为可映射到结构、字符串和数组的 Java 对象进行封送算法。Java 开发者将能够像调用原生 Java 方法一样调用原生库中的任何导出函数。唯一需要解决的任务是如何定义元数据,这些元数据将保留有关绑定到作为原生函数原型的 Java 原生方法的原生函数的信息。

作者用 C++ 编写了 Java Platform Invoke SDK 的 JNI 代码。它使用 Microsoft VC++ 和 MinGW 编译器进行编译,以便将来移植到 Linux 和 Unix 系统。

在 Java 中使用 Platform Invoke

Java 注解和 .NET 属性

从 JSE 1.5.x 开始,SUN 在 Java 语言标准中添加了注解,以消除在 Java 类中使用自定义属性。注解提供了有关 Java 类及其成员的信息,这些信息不是程序本身的一部分。它们对代码的运行没有直接影响,只是进行注解。注解有多种用途,其中包括

  • 编译器信息:编译器可以使用注解来检测错误或抑制警告。
  • 编译时和部署时处理:软件工具可以处理注解信息来生成代码、XML 文件等。
  • 运行时处理:某些注解可以在运行时进行检查。

用于运行时处理的注解类似于 .NET 语言中的运行时属性,它们定义了作为元数据存储的信息。在 Java 中,您可以注解类、方法、方法参数和类字段。与 .NET 属性一样,注解可以保留默认值。要在 .NET 导出的 DLL 函数中使用,您需要

  1. 标识 DLL 中的函数:最少,您必须指定函数的名称以及包含它的 DLL 的名称。
  2. 创建一个类来保存 DLL 函数:您可以使用现有类,为每个非托管函数创建一个单独的类,或者创建一个包含一组相关非托管函数的类。
  3. 在托管代码中创建函数原型。
  4. 调用 DLL 函数:像调用任何其他托管方法一样,在您的托管类上调用该方法,传递结构和实现的 callback 函数。

例如,C# 中 MessageBox 函数的定义可以这样写

[DllImport("User32.dll", EntryPoint= "MessageBox")]
static extern intMessageBox(inthwnd, stringmsg, stringcaption, intmsgtype);

使用 Java 注解,您可以编写相同的声明

@ImportLibrary(libName = "user32") 
public classUserLib extendsCNativeLibrary 
{ 
    @Function(entryPoint = "MessageBoxW") 
    public native intMessageBox(inthwnd, 
        String msg, String caption, intmsgtype); 
}

注解 @ImportLibrary 表示与类 UserLib 相对应的库的名称,即 libName 值。原生函数原型由 @Function 注解,参数 entryPoint = "MessageBoxW"。这定义了绑定到 Java 代码中原型的函数的原始名称。在这两个示例中,都没有封送定义。在调用 MessageBox 方法时,Platform Invoke SDK 使用默认的封送规则。在 C# 中,所有 string 参数都被封送为 ANSI 字符集,但在 Java 代码中,string 参数默认被封送和解封送为 Unicode 字符集。

函数原型和默认封送

如果 Platform Invoke 是 JVM 的一部分,它将看起来像 .NET 语言中的样子。然而,在我们的 SDK 中,我们部分在 Java 模块中实现它,其余部分在 JNI 模块中实现。也就是说,要声明一个原生函数原型或原型,您需要创建一个继承 CNativeLibrary 类的类,其中原生方法是函数的原型。此方法像任何其他 Java 原生方法一样被调用

UserLib user32 = newUserLib();
user32.MessageBox(0, "KUKU from Java", "Java Message", 1);

在处理对函数 MessageBox 的调用时,JavaPInvoke 执行以下一系列操作

  1. 它找到包含函数的库。
  2. 它将库加载到内存中。
  3. 它找到内存中函数的地址,并将参数——由 JVM 在原生函数原型调用时传递——压入堆栈,根据需要封送数据。
  4. 它将控制权转移到原生函数。
  5. 它获取返回的结果,根据需要进行解封送,并将结果传回 Java 代码。

上面的 MessageBox 函数原型没有显式的封送声明,JavaPInvoke 使用默认封送,即 string 是 Unicode 的输入输出值。

自定义封送

有时默认封送会降低 Java 代码的性能,因为函数参数可能只输入或只输出。一些输出指针可能是常量并作为指针返回。使用 JavaPInvoke 引擎删除它们可能会导致 GPF。使用自定义封送,MessageBox 函数原型可以修改为

@ImportLibrary(libName = "user32") 
public classUserLib extendsCNativeLibrary 
{ 
    @Function(entryPoint = "MessageBoxA", charSet = CharSet.Ansi) 
    public native intMessageBox(inthwnd, 
        @In String msg, @In String caption, intmsgtype); 
}

这里,原生方法 MessageBox 将绑定到函数的 ANSI 版本。String 参数,如输入值,仅封送为 ANSI string 值——即 charSet = CharSet.Ansi——并忽略解封送。

JavaPInvoke 的其他特性

JavaPInvoke SDK 支持数据类型的封送

  • 结构/联合
  • 数组
  • 带虚函数的原生类
  • 静态 callback 函数实现
  • 原生事件接口(开发中)

结构封送

JavaPInvoke SDK 包含一些简单的方法,用于将 Java 类成员封送为/从 C++ 结构/联合。我们从 .NET Platform Invoke 获取了基本思想。这里只描述简单的结构封送——即针对没有指针和嵌套结构的本地结构——但 JavaPInvoke SDK 还为开发人员提供了定义更复杂封送的方法。

托管和非托管数据结构仅通过数据布局兼容,任何 Java 类字段都可以定义为某个本地结构的一部分。结构用扩展 CStructure 的 Java 类定义。此类应被注解为 @StructLayout(layout = LayoutKind.Sequential)。例如,Microsoft Windows API SYSTIME 结构可以在 Java 中定义为

@StructLayout(layout = LayoutKind.Sequential) 
public classSYSTEMTIME extendsCStructure 
{
    public shortwYear; 
    public shortwMonth; 
    public shortwDayOfWeek; 
    public shortwDay; 
    public shortwHour; 
    public shortwMinute; 
    public shortwSecond; 
    public shortwMilliseconds; 
}

最好显式地将结构打包设置与本地结构 @StructLayout(pack=8, layout=LayoutKind.Sequential) 使用的设置相同。但是,大多数系统结构与结构打包无关。

静态 Callback 函数实现

一些原生函数可以接收 callback 函数指针作为参数。JavaPInvoke SDK 提供了创建原生包装器(一个原生的 callback 函数指针)的机制。要创建原生 callback 函数指针,您需要定义一个只有一个方法的类。也就是说,callback 函数的实现。例如,让我们实现与 EnumWindows 函数一起调用的 WNDENUMPROC。首先,定义一个实现 WNDENUMPROC 的类

public classWNDENUMPROC
{
    publicWNDENUMPROC()
    {
    }
    public booleanEnumWindowsProc(inthwnd, intlParam)
    {
        String shwnd = Integer.toHexString(hwnd);
        if(shwnd.length() <  8)
        {
            switch(8 - shwnd.length())
            {
            case1:
                shwnd = "0" + shwnd;
                break;
            case2:
                shwnd = "00" + shwnd;
                break;
            case3:
                shwnd = "000" + shwnd;
                break;
            case4:
                shwnd = "0000" + shwnd;
                break;
            case5:
                shwnd = "00000" + shwnd;
                break;
            case6:
                shwnd = "000000" + shwnd;
                break;
            case7:
                shwnd = "0000000" + shwnd;
                break;
            }
        }
        if(lParam == 1)
        System.out.println("EnumWindows: 0x" + shwnd);else
        System.out.println("EnumDesktopWindows: 0x" + shwnd);
        return true;
    }
}

然后在调用 EnumWindows 函数之前,创建 callback 函数对象

WNDENUMPROC proc = newWNDENUMPROC();

然后为这个 callback 函数创建一个包装器

CCallback enumer = newCCallback(proc, 
    WNDENUMPROC.class.getMethod("EnumWindowsProc", 
    newClass[]{int.class, int.class}), CallingConvention.Stdcall);

最后,调用在库对象 user32 中定义的 EnumWindows 函数

user32.EnumWindows(enumer, 1);

原生编码可以很简单

那些编写了带有非托管扩展的托管 C++ 代码的人应该会发现与 Java Native Interface 有很多共同之处。与托管 C++ 相比,JNI 有许多低级功能,使得编码和调试非常复杂。这些是

  • Java 引用(局部、全局、弱引用),它们应该在 JNI 代码中正确处理,并且在大多数情况下对于普通 Java 开发者来说并不清晰。
  • 数组封送:在 Java 论坛上,我总是会发现关于如何获取或返回 Java 数组的问题,尤其是多维数组。
  • Java 方法调用,其中 Java 开发者应该知道如何编写方法或字段签名等。

也就是说,高科技行业应该雇佣高素质且高薪的人员,他们了解 Java、C++、Pascal、汇编语言等,并且还了解 JNI 编程。我省略了公司特有的其他事项。然而,Java 提供商在开发复杂的 Java 库以消除 JNI 使用时并不关心这个问题。他们应该考虑 Java 扩展,以便直接从 Java 代码调用原生函数,而无需实现 Java 类原生方法。这将为 Java 开发者提供一个在 Java 中使用原生、系统依赖的函数的选项,而无需 JNI。Java 开发者可以只编写 Java 代码,而 C++(Pascal、汇编语言等)程序员将编写/调试纯原生模块,这些模块与 JNI 无关。

关注点

JavaPInvoke SDK 可用于在 Microsoft Windows 环境中开发 Java-COM 桥。尝试实现 IUnknownIDispatch 接口。然后,您将能够轻松创建 COM 对象并调用其原生函数。不要忘记在 Main 过程中调用 CNativeLoader.OleActivate()

其他资源

  • IBM, SUN JDK1.5.x 及更高版本
  • Java Platform Invoke 可在此 获得

参考文献

历史

  • 2007 年 7 月 24 日 -- 发布了原始版本
  • 2009 年 5 月 4 日 -- 文章已更新
© . All rights reserved.