使用内存映射文件和 JNI 在 Java 和 C++ 程序之间通信






4.90/5 (26投票s)
2002年6月19日
8分钟阅读

205718

2694
一篇关于Java程序之间、Java与C++程序之间进程间通信的文章。
引言
有时,您的Java应用程序需要相互通信,有时,您的Java应用程序需要与C++程序通信。
考虑以下场景
您有一个Java程序,我们称之为JServer。当发生某些有趣的事件时,JServer会通知客户端。客户端可以是其他Java程序或C++程序。这可能看起来像这样
JServer有一个文本区域。用户输入内容后,JServer会通知Java和C++客户端数据已准备好,请获取。
有许多方法可以解决这些问题。在这里,我将使用内存映射文件和JNI来完成这项工作。为什么使用内存映射文件,因为它效率高。为什么使用JNI,因为我不想将我的代码限制在Microsoft Java虚拟机。
警告:无论何时使用JNI,您的代码将不再是100%纯Java。但我对此没意见。 :)
桥梁:命名内存映射文件
您可以使用窗口消息(如WM_COPYDATA
)、管道、套接字(仅举几例)来在同一台机器上的不同进程之间共享数据。但最高效的方法是使用内存映射文件。因为上面提到的所有机制都在内部使用内存映射文件来完成繁重的工作。内存映射文件的唯一“问题”是,所有涉及的进程都必须使用完全相同的名称来命名文件映射内核对象。但这对我来说也没关系。 :)
用Java实现内存映射文件
JDK 1.4提供了一些内存映射功能,这很好,但还不够。在这里,我将介绍一个非常简单的Java类MemMapFile
。您可以轻松地扩展它来完成更复杂的工作。它具有以下字段和本地方法
public static final int PAGE_READONLY = 0x02;
public static final int PAGE_READWRITE = 0x04;
public static final int PAGE_WRITECOPY = 0x08;
public static final int FILE_MAP_COPY = 0x0001;
public static final int FILE_MAP_WRITE = 0x0002;
public static final int FILE_MAP_READ = 0x0004;
public static native int createFileMapping(int lProtect,
int dwMaximumSizeHigh, int dwMaximumSizeLow, String name);
public static native int openFileMapping(int dwDesiredAccess,
boolean bInheritHandle, String name);
public static native int mapViewOfFile(int hFileMappingObj,
int dwDesiredAccess, int dwFileOffsetHigh,
int dwFileOffsetLow, int dwNumberOfBytesToMap);
public static native boolean unmapViewOfFile(int lpBaseAddress);
public static native void writeToMem(int lpBaseAddress, String content);
public static native String readFromMem(int lpBaseAddress);
public static native boolean closeHandle(int hObject);
public static native void broadcast();
这些听起来很熟悉。是的,MemMapFile
只不过是Win32 API关于内存映射文件的Java包装器。您还会注意到,我没有对应的LPSECURITY_ATTRIBUTES
参数,这是因为我想让事情变得简单,或者我必须编写另一个包装类。在本地端,我将为此参数传递NULL
(在大多数情况下是可接受的),如下所示
JNIEXPORT jint JNICALL Java_com_stanley_memmap_MemMapFile_createFileMapping (JNIEnv * pEnv, jclass, jint lProtect, jint dwMaximumSizeHigh, jint dwMaximumSizeLow, jstring name) { HANDLE hFile = INVALID_HANDLE_VALUE; HANDLE hMapFile = NULL; LPCSTR lpName = pEnv->GetStringUTFChars(name, NULL); __try { hMapFile = CreateFileMapping(hFile, NULL, lProtect, dwMaximumSizeHigh, dwMaximumSizeLow, lpName); if(hMapFile == NULL) { ErrorHandler(_T("Can not create file mapping object")); __leave; } if(GetLastError() == ERROR_ALREADY_EXISTS) { ErrorHandler(_T("File mapping object already exists")); CloseHandle(hMapFile); __leave; } } __finally{ } pEnv->ReleaseStringUTFChars(name, lpName); // if hMapFile is NULL, just return NULL, or return the handle return reinterpret_cast<<code>jint>(hMapFile); }
当您获得文件映射对象的句柄时,需要将其转换为jint
类型并在Java端缓存。稍后需要时,可以将其转换回HANDLE
。以下是mapViewOfFile
的实现,您将看到我使用了createFileMapping
返回的HANDLE hMapFile
。
JNIEXPORT jint JNICALL Java_com_stanley_memmap_MemMapFile_mapViewOfFile (JNIEnv *, jclass, jint hMapFile, jint dwDesiredAccess, jint dwFileOffsetHigh, jint dwFileOffsetLow, jint dwNumberOfBytesToMap) { PVOID pView = NULL; pView = MapViewOfFile(reinterpret_cast<<code>HANDLE>(hMapFile), dwDesiredAccess, dwFileOffsetHigh, dwFileOffsetLow, dwNumberOfBytesToMap); if(pView == NULL) ErrorHandler(_T("Can not map view of file")); return reinterpret_cast<jint>(pView); }
pView
指针是一个扁平指针,您可以在该地址执行任何您想做的操作。我的writeToMem()
和readFromMem()
将简单地将一些字符串写入该内存并从内存中读取字符串。
当发生一些有趣的事情时,您会注意到您的客户端。您有很多方法可以做到这一点。我很懒,我只是在MemMapFile
中放了一个广播方法,它会广播一条消息,告知数据已准备好。
JNIEXPORT void JNICALL Java_com_stanley_memmap_MemMapFile_broadcast (JNIEnv *, jclass) { SendMessage(HWND_BROADCAST, UWM_DATA_READY, 0, 0); }
UWM_DATA_READY
是一个用户定义的消息。用户定义消息经常用于不同进程之间的协作。如果您不熟悉它,可以访问Dr. Newcomer的主页,他有一篇关于Windows消息管理的精彩文章。
在使用用户定义消息之前,必须先注册它。您的本地实现DLL入口点函数是注册自己的消息的一个好地方。因此,您将拥有类似以下内容
#define UWM_DATA_READY_MSG _T ("UWM_DATA_READY_MSG-{7FDB2CB4-5510-4d30-99A9-CD7752E0D680}") UINT UWM_DATA_READY; BOOL APIENTRY DllMain(HINSTANCE hinstDll, DWORD dwReasion, LPVOID lpReserved) { if(dwReasion == DLL_PROCESS_ATTACH) UWM_DATA_READY = RegisterWindowMessage(UWM_DATA_READY_MSG); return TRUE; }
好了,有了MemMapFile
,我们现在可以跨进程共享内存了,前提是这些进程知道文件映射内核对象的名称。
构建JServer
创建一个创建共享内存的C++服务器很容易。创建Java服务器更有趣。我们仍然称之为JServer。在您的JServer中,您需要某种方式来缓存从本地代码返回的原始指针。
private int mapFilePtr; private int viewPtr;
然后您可以像这样初始化它们
mapFilePtr = MemMapFile.createFileMapping(MemMapFile.PAGE_READWRITE, 0, dwMemFileSize, fileMappingObjName); if(mapFilePtr != 0) { viewPtr = MemMapFile.mapViewOfFile(mapFilePtr, MemMapFile.FILE_MAP_READ | MemMapFile.FILE_MAP_WRITE, 0, 0, 0); }
当您想通知您的客户端时,可以发布您刚刚注册的消息。在我的示例中,在我输入一些内容到文本区域后,我将单击“写入并广播”按钮。该按钮的行为如下
public void actionPerformed(ActionEvent e) { if(viewPtr != 0) { MemMapFile.writeToMem(viewPtr, textArea.getText()); MemMapFile.broadcast(); } }
文本区域中的内容将被写入共享内存,并发布数据准备就绪消息。
当您想关闭服务器时,不要忘记调用unmapViewOfFile()
和CloseHandle()
来释放资源并从您的进程地址空间中取消映射共享内存。
构建Java客户端
构建Java客户端并不直接。问题在于客户端如何获取数据准备就绪的消息。仍然有许多方法可以做到这一点。在本例中,我将使用一个隐藏窗口和JNI回调来处理消息。
步骤1:定义一个接口MemMapFileObserver。
我将在步骤2中解释为什么使用接口。
public interface MemMapFileObserver { public void onDataReady(); }
步骤2:定义一个代理MemMapProxy。
MemMapProxy
将处理数据准备就绪的消息。当然,我再次使用JNI。
public class MemMapProxy { static { System.loadLibrary("MemMapProxyLib"); } private MemMapFileObserver observer; public MemMapProxy(MemMapFileObserver observer) { this.observer = observer; init(); } public void fireDataReadyEvent() { observer.onDataReady(); } private native boolean init(); public native void destroy(); }
现在您应该知道为什么我使用的是Observer
接口。我只是想让代码更通用。任何对数据准备就绪消息感兴趣的客户端都可以实现此接口。在我的代理类中,我只有一个观察者。如果您愿意,可以使用EventListenerList
。
MemMapProxy
类看起来很简单,而棘手的部分隐藏在init()
本地方法中。
JNIEXPORT jboolean JNICALL Java_com_stanley_memmap_MemMapProxy_init (JNIEnv * pEnv, jobject jobj) { HANDLE hThread; hThread = (HANDLE)_beginthreadex(NULL, 0, &CreateWndThread, NULL, 0, &uThreadId); if(!hThread) { MessageBox(NULL, _T("Fail creating thread"), NULL, MB_OK); return false; } g_jobj = pEnv->NewGlobalRef(jobj); return true; } unsigned WINAPI CreateWndThread(LPVOID pThreadParam) { HANDLE hWnd = CreateWindow(_T("dummy window"), NULL, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL); if(hWnd == NULL) { MessageBox(NULL, _T("Failed create dummy window"), NULL, MB_OK|MB_ICONERROR); return 0; } jint nSize = 1; jint nVms; jint nStatus = JNI_GetCreatedJavaVMs(&g_pJvm, nSize, &nVms); if(nStatus == 0) { nStatus = g_pJvm->AttachCurrentThread (reinterpret_cast<void**>(&g_pEnv), NULL); if(nStatus != 0) ErrorHandler(_T("Can not attach thread")); } else { ErrorHandler(_T("Can not get the jvm")); } MSG Msg; while(GetMessage(&Msg, 0, 0, 0)) { TranslateMessage(&Msg); DispatchMessage(&Msg); } return Msg.wParam; } // cache the JNIEnv* pointer JNIEnv* g_pEnv = NULL; // cache the JavaVM* pointer JavaVM* g_pJvm = NULL; // cache the jobject(MemMapProxy) pointer jobject g_jobj = NULL;
正如您所见,init()
将启动一个“守护线程”。该线程将创建一个隐藏窗口并初始化一些全局变量,然后进入消息循环。守护线程将一直运行,直到收到WM_QUIT
消息。我为什么要这样做?原因是,在init()
返回后,我需要一些东西能够始终运行以监视窗口的消息。
在创建隐藏窗口之前,我需要做两件事。首先,我必须注册一个窗口类。在我的示例中,这个类名为“dummy window”。其次,我必须注册与MemMapFile
中相同的UWM_DATA_READY
消息。我像之前一样在我的DLL入口点函数中注册我的窗口类和消息。
我的隐藏窗口的窗口函数很简单
LRESULT CALLBACK WndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) { if(Msg == UWM_DATA_READY) { Callback(); } else return DefWindowProc(hWnd, Msg, wParam, lParam); return 0; } void Callback() { if(g_pEnv == NULL || g_jobj == NULL) return; jclass cls = g_pEnv->GetObjectClass(g_jobj); jmethodID mid = g_pEnv->GetMethodID(cls, "fireDataReadyEvent", "()V"); g_pEnv->CallVoidMethod(g_jobj, mid, NULL); }
当它收到UWM_DATA_READY
消息时,它将调用我的Callback()
。Callback()
是一个回调函数,它将调用我的Java端中的fireDataReadyEvent()
。fireDataReadyEvent()
反过来会调用观察者的onDataReady()
方法。
JNI回调与COM中的IDispatch
非常相似。您必须首先获取方法ID,基于方法名,然后才能调用该方法。然而,当涉及多线程时,事情会变得更有趣。首先,JNIEnv*
指针仅在当前线程中有效。其次,您不能将局部引用传递给另一个线程。我们的回调发生在我们的守护线程中,所以我们必须找到一种方法来获取JNIEnv*
指针并获取对我们代理对象的引用。
您一定注意到我的DLL中有几个全局变量,g_jobj
、g_pEnv
和g_pJvm
。在我的init()
中,我通过创建一个新的全局引用来初始化g_jobj
。然后我可以将其传递给我的守护线程。在我的CreateWndThread()
中,我可以通过JNI_GetCreatedJavaVMs()
获取JVM的指针,然后通过将我的守护线程附加到JVM,我可以获得JNIEnv*
指针。好了,到目前为止还不错。 :)
步骤3:创建Java客户端
创建Java客户端很简单。我需要做的是实现MemMapFileObserver
接口。
public void onDataReady() { int mapFilePtr = MemMapFile.openFileMapping(MemMapFile.FILE_MAP_READ, false, fileMappingObjName); if(mapFilePtr != 0) { int viewPtr = MemMapFile.mapViewOfFile(mapFilePtr, MemMapFile.FILE_MAP_READ, 0, 0, 0); if(viewPtr != 0) { String content = MemMapFile.readFromMem(viewPtr); textArea.setText(content); MemMapFile.unmapViewOfFile(viewPtr); } MemMapFile.closeHandle(mapFilePtr); } }
构建C++客户端
构建C++客户端很简单。在我的示例中,我使用的是一个MFC对话框。它有一个CEdit
控件。它也会注册UWM_DATA_READY
消息。当我收到这条消息时,我会读取共享内存并将内容复制到我的编辑控件中。以下是我的onDataReady()
LRESULT CMemMapCppClientDlg::OnDataReady(WPARAM, LPARAM) { HANDLE hMapFile = NULL; PVOID pView = NULL; hMapFile = OpenFileMapping(FILE_MAP_READ, FALSE, m_pszMemMapFileName); if(hMapFile == NULL) { MessageBox("Can not open file mapping"); return 0; } pView = MapViewOfFile(hMapFile, FILE_MAP_READ, 0, 0, 0); if(pView == NULL) { MessageBox("Can map view of file"); CloseHandle(hMapFile); return 0; } LPSTR szContent = reinterpret_cast<<code>LPSTR>(pView); int nLen = strlen(szContent); CString strContent; while(nLen > 0) { strContent += *szContent++; --nLen; } strContent += '\0'; strContent.Replace("\n", "\r\n"); m_edit.SetWindowText(strContent); if(pView) UnmapViewOfFile(pView); if(hMapFile) CloseHandle(hMapFile); return 0; }
我想解释的一件事是,您需要将换行符('\n')替换为回车符('\r')后跟一个换行符('\n'),以使内容兼容。
内存同步
在我的简单示例中,没有主要的同步问题。然而,如果您的场景更复杂,或者您将遇到麻烦,这将是您的大问题。
您不能仅使用临界区来保护您的资源,因为临界区只能同步同一进程内的线程。然而,使用互斥体和信号量内核对象来保护您的数据并不难。如果您更关心效率,可以使用临界区和内核对象一起使用。如果您向MemMapFile
添加更多本地函数,例如Wait函数和CreateMutex()
……包装相应的Win32API,那将更有趣。这对您来说是一个很好的练习。 :)
结论
在本文中,我使用了一个简单的示例来说明如何通过共享内存和JNI在Java与Java、Java与C++程序之间进行通信。我没有使用Microsoft Visual J++和com.ms.xxx.xxxx之类的东西,因为我想使用Sun JVM。您可以轻松地扩展我的示例并将其应用于更复杂的场景。