集成 Visual C++、Java 和汇编






4.87/5 (32投票s)
本文介绍了一种集成 Java、C++ 和汇编的方法
VC++、Java 和汇编的接口
几周前,我参与了一个涉及 Java 的项目。将 VC++、Java 和汇编集成在一起完成某项工作是一次愉快的经历。因此,我决定重写我之前用 C# 调用汇编语言(借助 VC)的文章,现在我将借助 VC++、Java 和汇编,通过 JNI(Java Native Interface)来实现同样的功能。
但基本问题是一样的,为什么我们要从 Java 调用汇编语言?可能有以下原因:
- 您将要进行一些系统级别的操作,而 Java 本身不支持这些操作,例如调用汇编语言。
- 有些代码已经编写好了,您只想在您的 Java 应用程序中使用它们。
因此,您将使用 JNI(Java Native Interface)从 Java 执行本机代码。但是,请注意,当您计划从 Java 程序调用本机函数时,您的程序将不再是可移植的。
要从 Java 调用本机代码,您必须遵循以下步骤:
- 编写 Java 程序,在其中声明您将在程序中调用的本机方法。
- 编译 Java 程序以生成类文件。
- 使用 javah 工具从 Java 类文件创建 C++ 头文件,以便在 C++ 程序中使用。
- 编写 C++ 程序,即使用该头文件的 DLL。
现在有一个问题,我们如何将现有的 DLL 用于 Java 程序?因为这些 DLL 不是为了使用 Java 创建的头文件而编写的。这个问题的解决方案是创建一个包装器 DLL,它只是调用您的 DLL 中的函数。
让我们一步一步地讨论这些步骤。
要在 Java 中声明任何函数为本机函数,您在该函数中使用 `native` 关键字,并且不声明该函数的实现体。
程序 1
class prog1 { public static native void test(); public static void main(String args[]) { System.out.println("Hello World!"); } }
声明本机函数为 `static` 不是必需的,您也可以将其声明为非 `static` 函数。 `static` 本机函数和非 `static` 本机函数之间唯一的区别在于调用函数时。所以我们将在调用函数时看到区别。现在在命令行编译这个程序
Javac prog1.java
此程序的输出是类文件。这是一个可以运行的程序,如果您愿意,可以通过键入以下命令来运行它:
Java prog1
现在第二步是为 C/C++ 生成头文件。Java 自带一个名为 `javah` 的实用程序,它可以从 Java 类文件创建 C/C++ 的头文件。在命令提示符下键入此命令:
Javah prog1
这将创建一个同类文件同名的头文件,即 `prog1.h`。这个头文件非常简单,它声明了在 Java 程序中所有声明为 `native` 的函数的原型。头文件如下所示:
Prog1.h
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class prog1 */ #ifndef _Included_prog1 #define _Included_prog1 #ifdef __cplusplus extern "C" { #endif /* * Class: prog1 * Method: test * Signature: ()V */ JNIEXPORT void JNICALL Java_prog1_test (JNIEnv *, jclass); #ifdef __cplusplus } #endif #endif
您应该在 JNI 基础 DLL 中至少包含一个头文件,即 `jni.h`。这个头文件在您使用 `javah` 工具创建头文件时会自动包含。 `JNIEXPORT` 定义为 `__declspec(dllexport)`,而 `JNICALL` 是在 `jni_md.h` 中定义的标准调用约定。
#define JNIEXPORT __declspec(dllexport) #define JNICALL __stdcall
所以函数名大致是这样的:
__declspec(dllexport) void stdcall Java_prog1_test(JNIEnv*, jclass)
函数名以 `Java` 开头,后跟包名,然后是类名,最后是 Java 文件中声明为 `native` 的函数名。这里我们没有定义任何函数,所以在导出的函数中没有包名。任何 JNI 函数的第一个参数都是指向 JNIEnv 结构体的指针。这个结构体用于在 C++ 程序中使用与 Java 环境相关的函数,例如,字符串在 Java 和 C++ 中的存储方式不同,因此您必须在调用 JNI 定义的函数将其转换为适当的类型才能使用。
现在在 VC 中创建一个 DLL 项目,并将此头文件包含在项目中。并为这些本机函数提供实现。请记住,您可以为此 cpp 文件使用任何名称,不一定需要使用与类文件名或头文件名相同的名称。
prog1.cpp
#include <windows.h> #include <stdio.h> #include "prog1.h" BOOL WINAPI DllMain(HANDLE hHandle, DWORD dwReason, LPVOID lpReserved) { return TRUE; } JNIEXPORT void JNICALL Java_prog1_test(JNIEnv *, jclass) { printf("Hello world from VC++ DLL\n"); }
编译此项目后,您将获得所需的 DLL 文件。接下来是如何在 Java 程序中使用此 DLL 及其函数。加载 DLL 的 API 是 `loadLibrary`。现在程序大致是这样的:
程序 2
class prog1 { static { System.loadLibrary("test.dll"); } public static native void test(); public static void main(String args[]) { System.out.println("Hello World!"); test(); } }
当您运行此程序时,程序会在抛出 `UnsatisfiedLinkError` 异常后崩溃。因此,让我们捕获这个异常并稍微修改一下我们的程序。
程序 3
class prog1 { static { try { System.loadLibrary("test.dll"); } catch(UnsatisfiedLinkError ule) { System.out.println(ule); } } public static native void test(); public static void main(String args[]) { System.out.println("Hello World!"); test(); } }该程序的输出是
java.lang.UnsatisfiedLinkError: no test.dll in java.library.path Hello World! Exception in thread "main" java.lang.UnsatisfiedLinkError: test at prog1.test(Native Method) at prog1.main(prog1.java:19)
此程序表明它找不到我们刚刚制作的 DLL。请务必将 DLL 复制到路径或当前文件夹。但是,即使您将 DLL 复制到同一文件夹,您也会收到相同的错误。原因是您不应该在 `loadLibrary` 函数中编写 DLL 的扩展名。
程序 4
class prog1 { static { try { System.loadLibrary("test"); } catch(UnsatisfiedLinkError ule) { System.out.println(ule); } } public static native void test(); public static void main(String args[]) { System.out.println("Hello World!"); test(); } }
该程序的输出是
Hello World! Hello world from VC++ DLL
现在来看非 `static` 的本机函数。实际上,您也可以将本机函数声明为非 `static`。但在这种情况下,您必须创建 `prog1` 类的实例并调用本机函数。
程序 5
class prog1 { static { try { System.loadLibrary("test"); } catch(UnsatisfiedLinkError ule) { System.out.println(ule); } } public native void test(); public static void main(String args[]) { System.out.println("Hello World!"); new prog1().test(); } }
程序 4 和 5 唯一的区别在于本机函数的声明和调用。上述程序的输出与前面的程序相同。但我将在剩余的文章中使用 `static` 的本机函数。
好的,让我们尝试一些传递参数并向本机函数返回内容的操作。让我们编写一个程序来调用一个本机函数,该函数对两个数字进行求和并返回结果。
程序 6
class prog1 { static { try { System.loadLibrary("test"); } catch(UnsatisfiedLinkError ule) { System.out.println(ule); } } public static native int Sum(int a, int b); public static void main(String args[]) { System.out.println(Sum(5, 10)); } }
这是用于创建 DLL 的 CPP 文件:
程序 7
#include <windows.h> #include "prog1.h" BOOL WINAPI DllMain(HANDLE hHandle, DWORD dwReason, LPVOID lpReserved) { return TRUE; } JNIEXPORT jint JNICALL Java_prog1_Sum(JNIEnv *, jclass, jint a, jint b) { return a + b; }
该程序的输出是
15
处理整数很容易。现在让我们对字符流做一些实验。让我们编写一个本机函数,它接受一个字符串作为参数,并在将其大写后返回它。
程序 8
class prog1 { static { try { System.loadLibrary("test"); } catch(UnsatisfiedLinkError ule) { System.out.println(ule); } } public static native String saySomething(String strString); public static void main(String args[]) { System.out.println(saySomething("Hello world")); System.out.println(saySomething("Bye world")); } }
这是用于此的 CPP 程序:
程序 9
#include <windows.h> #include <string.h> #include "prog1.h" BOOL WINAPI DllMain(HANDLE hHandle, DWORD dwReason, LPVOID lpReserved) { return TRUE; } JNIEXPORT jstring JNICALL Java_prog1_saySomething(JNIEnv * env, jclass, jstring strString) { char *lpBuff = (char*)env->GetStringUTFChars(strString, 0); _strupr(lpBuff); jstring jstr = env->NewStringUTF(lpBuff); env->ReleaseStringUTFChars(strString, lpBuff); return jstr; }
程序输出是:
HELLO WORLD BYE WORLD
此程序中的重要部分是使用 `GetStringUTFChars`、`ReleaseStringUTFChars` 和 `NewStringUTF` 函数。 `GetStringUTFChars` 将字符表示从 Java 的 Unicode 表示转换为 C 语言的以 Null 结尾的字符串。您必须调用 `ReleaseStringUTFChars` 来释放虚拟机分配的内存。如果您忘记调用此函数,将会导致内存泄漏。 `NewStringUTF` 用于创建函数返回的新字符串。
现在我们有了足够的知识来将 C# 程序移植到 Java。让我们编写 Java 程序来创建头文件。
程序 10
class sysInfo { static { try { System.loadLibrary("SysInfo"); } catch(UnsatisfiedLinkError ule) { System.out.println(ule); } } public static native int getCPUSpeed(); public static native String getCPUType(); public static native int getCPUFamily(); public static native int getCPUModal(); public static native int getCPUStepping(); public static void main(String args[]) { System.out.println("Information about System"); System.out.println("========================"); System.out.println("Get CPU Speed: " + getCPUSpeed()); System.out.println("Get CPU Type: " + getCPUType()); System.out.println("Get CPU Family: " + getCPUFamily()); System.out.println("Get CPU Modal: " + getCPUModal()); System.out.println("Get CPU Stepping: " + getCPUStepping()); } }
这是使用此程序创建的头文件的 CPP 程序:
程序 11
#include <windows.h> #include "sysinfo.h" BOOL WINAPI DllMain(HANDLE hHandle, DWORD dwReason, LPVOID lpReserved) { return TRUE; } JNIEXPORT jint JNICALL Java_sysInfo_getCPUSpeed(JNIEnv *, jclass) { LARGE_INTEGER ulFreq, ulTicks, ulValue, ulStartCounter, ulEAX_EDX, ulResult; // it is number of ticks per seconds QueryPerformanceFrequency(&ulFreq); // current valueofthe performance counter QueryPerformanceCounter(&ulTicks); // calculate one second interval ulValue.QuadPart = ulTicks.QuadPart + ulFreq.QuadPart; // read time stamp counter // this asm instruction load the highorder 32 bit of the register into EDX // and the lower order 32 bits into EAX _asm { rdtsc mov ulEAX_EDX.LowPart, EAX mov ulEAX_EDX.HighPart, EDX } // start no of ticks ulStartCounter.QuadPart = ulEAX_EDX.QuadPart; // loop for 1 second do { QueryPerformanceCounter(&ulTicks); } while (ulTicks.QuadPart <= ulValue.QuadPart); // get the actual no of ticks _asm { rdtsc mov ulEAX_EDX.LowPart, EAX mov ulEAX_EDX.HighPart, EDX } // calculate result ulResult.QuadPart = ulEAX_EDX.QuadPart - ulStartCounter.QuadPart; return (int)ulResult.QuadPart / 1000000; } JNIEXPORT jstring JNICALL Java_sysInfo_getCPUType(JNIEnv * env, jclass) { static char pszCPUType[13]; memset(pszCPUType, 0, 13); _asm { mov eax, 0 cpuid // getting information from EBX mov pszCPUType[0], bl mov pszCPUType[1], bh ror ebx, 16 mov pszCPUType[2], bl mov pszCPUType[3], bh // getting information from EDX mov pszCPUType[4], dl mov pszCPUType[5], dh ror edx, 16 mov pszCPUType[6], dl mov pszCPUType[7], dh // getting information from ECX mov pszCPUType[8], cl mov pszCPUType[9], ch ror ecx, 16 mov pszCPUType[10], cl mov pszCPUType[11], ch } pszCPUType[12] = '\0'; return env->NewStringUTF(pszCPUType); } JNIEXPORT jint JNICALL Java_sysInfo_getCPUFamily(JNIEnv *, jclass) { int retVal; _asm { mov eax, 1 cpuid mov retVal, eax } return (retVal >> 8); } JNIEXPORT jint JNICALL Java_sysInfo_getCPUModal(JNIEnv *, jclass) { int retVal; _asm { mov eax, 1 cpuid mov retVal, eax } return ((retVal >> 4 ) & 0x0000000f); } JNIEXPORT jint JNICALL Java_sysInfo_getCPUStepping(JNIEnv *, jclass) { int retVal; _asm { mov eax, 1 cpuid mov retVal, eax } return (retVal & 0x0000000f); }
此程序在我的计算机上的输出如下,您可能会根据您使用的计算机获得不同的结果。还有一件事,我使用了 CPUID 指令来识别微处理器的供应商名称,该指令仅在 Pentium 及以上微处理器上可用,因此如果您使用的微处理器低于 Pentium,您可能会得到不可预测的结果。
Information about System ======================== Get CPU Speed: 1003 Get CPU Type: AuthenticAMD Get CPU Family: 6 Get CPU Modal: 4 Get CPU Stepping: 2
致谢
感谢 Tasnim Ahmed(SoftPakSys 的 Java 团队领导)回答了我关于 Java 的问题,Khuram Rehmani 在 JNI 的使用方面给我提供了一些建议,Muhammad Kashif Shafiq 在发布前审阅了本文。