cout << "Hello World!" << endl; // 张鲁夺 :: 个人博客,幸福着你的幸福!


从DLL中返回非固定长度字符串的JAVA及C#调用方案

作者:鲁夺,2017年6月15日,原创文章,转载请注明出处!

原文:http://zhangluduo.com/article/3848e428/


最近的项目中,需要将自己用C语言写好的DLL提供给JAVA程序员调用,在此过程中遇到了一些问题,逐一记录下来,以儆效尤。

首先,来看看从DLL中返回字符串的典型做法。

    void TestFunction(
        CHAR* Buffer,  // 调用者提供的内存区
        DWORD Size);   // 调用者提供的内存大小

调用如下:

CHAR Buffer[20] = {0};
TestFunction(Buffer, 20);

这种情况,仅适用于调用者对于DLL中函数输出的字符串大小是可知的。但是在实际的应用中,很多时候,从函数中返回的字符串长度是不可知的,例如从服务器上读回的json或xml数据。 显然,由调用者提供内存及内存大小是不合适的。一旦用户提供的内存过大,会造成浪费,而内存过小,不能完全容纳返回的数据。

解决办法是利用二级指针。DLL示例代码如下:

    void __stdcall C_GetString(CHAR** Text)
    {
    	if (!Text)
    		return;
    
    	CHAR* SampleText = "The string returned from the TestDLL.dll";
    
    	DWORD Len = strlen(SampleText);
    	CHAR* Buffer = new CHAR[Len + 1];
    	memset(Buffer, 0, Len + 1);
    	memcpy(Buffer, SampleText, Len);
    
    	{
    		CHAR Msg[100] = {0};
    		sprintf(Msg, "C_GetString, 返回给用户的内存地址是:0x%08X", Buffer);
    		MessageBox(NULL, Msg, "C_GetString", MB_OK);
    	}
    
    	*Text = Buffer;
    }
    
    void __stdcall C_GetString_Release(CHAR** Text)
    {
    	if (Text && *Text)
    	{
    		{
    			CHAR Msg[100] = {0};
    			sprintf(Msg, "C_GetString_Release, 将要释放的内存地址是:0x%08X", *Text);
    			MessageBox(NULL, Msg, "C_GetString_Release", MB_OK);
    		}
    
    		delete (*Text);
    		*Text = NULL;
    	}
    }

下面是MFC调用示例代码:

    void CTestCall_MFCDlg::OnButton1() 
    {
    	CHAR* s = NULL;
    	C_GetString(&s);
    	if (s)
    	{
    		AfxMessageBox(s);
    	}
    
    	C_GetString_Release(&s);
    }

使用C系列的语言调用没有任何问题。再来看看通过JNA调用的JAVA示例代码:

    import com.sun.jna.Library;
    import com.sun.jna.Native;
    import com.sun.jna.Platform;
    
    public interface TestDLL extends Library {
        	TestDLL INSTANCE = (TestDLL)
                Native.loadLibrary((Platform.isWindows() ? "TestDLL" : "c"),
                TestDLL.class);
    
        	//
        	// 声明DLL里面的函数原型
        	//
        	
        	void C_GetString(String[] s);
        	void C_GetString_Release(String[] s);
    }

调用如下:


	String[] s = new String[1];
	TestDLL.INSTANCE.C_GetString(s);
	System.out.println(s[0]);
	TestDLL.INSTANCE.C_GetString_Release(s);

执行结果如下:

看出问题了吗?DLL返回给上层调用者的内存地地址,和用户交还回来的内存地不一样。这肯定是不行的!会引起宿主进程的崩溃。 为什么会这样?这应该和JAVAJ中参数传递的“值传递”方式有关。当调用者调用C_GetString_Release(s);这一句代码时,传递进来的s,从程序运行结果来看,肯定是按值方式传递的,而不是按引用传递的。 JAVA对数据做了一层面向对象封装,这一点和C语言有很大不同。

那怎么解决这个问题?

我们可以将DLL改造一下,将分配的内存地址,通过“值”的方式返回给调用者。我们针对JAVA提供两个导出函数,如下:

    DWORD __stdcall JAVA_GetString(CHAR** Text)
    {
    	C_GetString(Text);
    	DWORD MemAddr = (DWORD)(*Text);
    	return MemAddr;
    }
    
    void __stdcall JAVA_GetString_Release(DWORD MemAddr)
    {
    	CHAR* p = (CHAR*)(DWORD*)MemAddr;
    
    	{
    		CHAR Msg[100] = {0};
    		sprintf(Msg, "JAVA_GetString_Release, 将要释放的内存地址是:0x%08X", p);
    		MessageBox(NULL, Msg, "JAVA_GetString_Release", MB_OK);
    	}
    
    	delete p;
    	p = NULL;
    }

现在,来看看JAVA如何调用?

    import com.sun.jna.Library;
    import com.sun.jna.Native;
    import com.sun.jna.Platform;
    
    public interface TestDLL extends Library {
        	TestDLL INSTANCE = (TestDLL)
                Native.loadLibrary((Platform.isWindows() ? "TestDLL" : "c"),
                TestDLL.class);
    
        	//
        	// 声明DLL里面的函数原型
        	//
    
        	Long JAVA_GetString(String[] s);
        	void JAVA_GetString_Release(Long MemAddr);
    }

调用如下:

	String[] s = new String[1];
    	Long MemAddr = TestDLL.INSTANCE.JAVA_GetString(s);
    	System.out.println(s[0]);
    	TestDLL.INSTANCE.JAVA_GetString_Release(MemAddr);

执行结果如下:

通过调试,返回给用户的内存地址,和用户交还回来的内存地址是一致的,内存释放也没有任何问题。

—————————————–
补充:
—————————————–

因为对JAVA不熟悉,不知道JAVA里有Pointer这个类型。现在有新的调用方案。如下:

    public interface TestDLL2 extends Library {
    	TestDLL2 INSTANCE = (TestDLL2)
                Native.loadLibrary((Platform.isWindows() ? "TestDLL" : "c"),
                		TestDLL2.class);
    
        	//
        	// 声明DLL里面的函数原型
        	//
        	
        	void C_GetString(Pointer[] s);
        	void C_GetString_Release(Pointer[] s);
    }

调用如下:

    Pointer[] s = new Pointer[1];
    TestDLL2.INSTANCE.C_GetString(s);
    if (s[0] != null)
    	System.out.println(s[0].getString(0));
    TestDLL2.INSTANCE.C_GetString_Release(s);

// 再补充一下C#的调用代码

    [DllImport("TestDLL.dll", CharSet = CharSet.Ansi)]
    public static extern void C_GetString(ref IntPtr s);
    
    [DllImport("TestDLL.dll", CharSet = CharSet.Ansi)]
    public static extern void C_GetString_Release(ref IntPtr s);
    
    IntPtr p = new IntPtr();
    C_GetString(ref p);
    if (p != IntPtr.Zero)
    {
    	string s = Marshal.PtrToStringAnsi(p);
    	MessageBox.Show(s);
    }
    else
    {
    	MessageBox.Show("NULL");
    }
    
    C_GetString_Release(ref p);

最后,提供一下本文配套的示例代码工程。点此下载

感谢向原创作者打赏!


Copyright © 2015 Zhang Luduo.

All rights reserved.