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

集成 Visual C++、Java 和汇编

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (32投票s)

2002年12月11日

CPOL

6分钟阅读

viewsIcon

154051

downloadIcon

1790

本文介绍了一种集成 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 在发布前审阅了本文。

© . All rights reserved.