在托管代码和非托管代码之间传递字符串





5.00/5 (28投票s)
如何将字符串传递给托管和非托管代码之间
背景
读者应具备 C# 和非托管 C++ 的基本知识。
返回 BSTR
一种从非托管 C++ DLL 返回 `string` 的简单方法是使用 `BSTR` 类型。
以下 C++ 导出函数返回一个包含版本 `string` 的 `BSTR`
extern BSTR __stdcall GetVersionBSTR()
{
return SysAllocString(L"Version 3.1.2");
}
.DEF 文件内容如下
LIBRARY
EXPORTS
GetVersionBSTR
导出函数在 .NET 应用程序中导入如下
namespace DemoApp.Model
{
static class ImportLibrary
{
const String DLL_LOCATION = "DemoLibrary.dll";
[DllImport(DLL_LOCATION, CharSet = CharSet.Ansi,
CallingConvention = CallingConvention.StdCall)]
[return: MarshalAs(UnmanagedType.BSTR)]
public static extern string GetVersionBSTR();
}
}
托管代码调用导入的函数如下
string version = Model.ImportLibrary.GetVersionBSTR();
托管代码将 `string` 作为 `BSTR` 进行封送,并在不再需要时释放内存。
从非托管代码调用导出函数时,应释放 `BSTR`,否则将导致内存泄漏。
返回 char *
封送 `char *` 返回值更为困难,原因在于 .NET 应用程序不知道内存是如何分配的,因此也不知道如何释放它。安全的方法是将返回的 `char *` 视为指向内存位置的指针。.NET 应用程序不会尝试释放内存。如果托管代码在堆上分配了 `string`,这当然存在内存泄漏的风险。
以下 C++ 导出函数返回一个定义为 `string` 字面量的版本 `string`
extern char * __stdcall GetVersionCharPtr()
{
return "Version 3.1.2";
}
对应的 .DEF 文件内容如下
LIBRARY
EXPORTS
GetVersionCharPtr
导出函数在 .NET 应用程序中导入如下
namespace DemoApp.Model
{
static class ImportLibrary
{
const String DLL_LOCATION = "DemoLibrary.dll";
[DllImport(DLL_LOCATION, CharSet = CharSet.Ansi,
CallingConvention = CallingConvention.StdCall)]
public static extern IntPtr GetVersionCharPtr();
}
}
托管代码调用导入的函数如下
IntPtr intPtr = Model.ImportLibrary.GetVersionCharPtr();
string version = System.Runtime.InteropServices.Marshal.PtrToStringAnsi(intPtr);
将字符串作为 BSTR 参数传递
使用 `BSTR` 类型将 `string` 作为参数传递非常简单。
以下 C++ 导出函数接受一个 `BSTR` 参数
extern void __stdcall SetVersionBSTR(BSTR version)
{
// Do something here ..
}
非托管代码不应释放 `BSTR`。
.DEF 文件内容如下
LIBRARY
EXPORTS
SetVersionBSTR
此函数导入到 C# 应用程序中如下
namespace DemoApp.Model
{
static class ImportLibrary
{
const String DLL_LOCATION = "DemoLibrary.dll";
[DllImport(DLL_LOCATION, CharSet = CharSet.Ansi,
CallingConvention = CallingConvention.StdCall)]
public static extern void SetVersionBSTR
([MarshalAs(UnmanagedType.BSTR) string version);
}
}
托管代码调用导入的函数如下
Model.ImportLibrary.SetVersionBSTR("Version 1.0.0);
将字符串作为 char * 参数传递
以下 C++ 导出函数接受一个 `char *` 参数
extern void __stdcall SetVersionCharPtr(char *version)
{
// Do something here ..
}
.DEF 文件内容如下
LIBRARY
EXPORTS
SetVersionCharPtr
此函数导入到 C# 应用程序中如下
namespace DemoApp.Model
{
static class ImportLibrary
{
const String DLL_LOCATION = "DemoLibrary.dll";
[DllImport(DLL_LOCATION, CharSet = CharSet.Ansi,
CallingConvention = CallingConvention.StdCall)]
public static extern void SetVersionCharPtr
([MarshalAs(UnmanagedType.LPStr) string version);
}
}
托管代码调用导入的函数如下
Model.ImportLibrary.SetVersionCharPtr("Version 1.0.0);
使用 BSTR * 参数返回字符串
非托管 C++ DLL 可以使用 `BSTR *` 参数将 `string` 返回给调用者。DLL 分配 `BSTR`,调用者负责释放。
以下 C++ 导出函数使用 `BSTR *` 类型的参数返回一个 `string`
extern HRESULT __stdcall GetVersionBSTRPtr(BSTR *version)
{
*version = SysAllocString(L"Version 1.0.0");
return S_OK;
}
.DEF 文件内容如下
LIBRARY
EXPORTS
GetVersionBSTRPtr
此函数导入到 C# 应用程序中如下
namespace DemoApp.Model
{
static class ImportLibrary
{
const String DLL_LOCATION = "DemoLibrary.dll";
[DllImport(DLL_LOCATION, CharSet = CharSet.Ansi,
CallingConvention = CallingConvention.StdCall)]
public static extern int GetVersionBSTRPtr
([MarshalAs(UnmanagedType.BSTR) out string version);
}
}
在 C# 代码中使用此函数很简单
string version;
Model.ImportLibrary.GetVersionBSTRPtr(out version);
托管代码将自动处理内存管理。
将字符串作为 char** 参数传递
以下 C++ 导出函数使用 `char **` 参数返回一个版本 `string`
extern HRESULT __stdcall GetVersionCharPtrPtr(char **version)
{
*version = "Version 1.0.0";
return S_OK;
}
.DEF 文件内容如下
LIBRARY
EXPORTS
GetVersionCharPtrPtr
该函数导入到托管代码如下
namespace DemoApp.Model
{
static class ImportLibrary
{
const String DLL_LOCATION = "DemoLibrary.dll";
[DllImport(DLL_LOCATION, CharSet = CharSet.Ansi,
CallingConvention = CallingConvention.StdCall)]
public static extern void GetVersionCharPtrPtr(out IntPtr version);
}
}
在 C# 代码中使用此函数很简单
IntPtr intPtr;
Model.ImportLibrary.GetVersionCharPtrPtr(out intPtr);
string version = System.Runtime.InteropServices.Marshal.PtrToStringAnsi(intPtr);
很明显,如果非托管 DLL 在堆上分配了 `string` 的内存,就会存在内存泄漏的风险。
使用缓冲区传递字符串
从非托管 C++ DLL 返回 `string` 的一种安全方法是使用调用者分配的缓冲区。例如
extern void __stdcall GetVersionBuffer(char *buffer, unsigned long *pSize)
{
if (pSize == nullptr)
{
return;
}
static char *version = "Version 5.1.1";
unsigned long size = strlen(version) + 1;
if ((buffer != nullptr) && (*pSize >= size))
{
strcpy_s(buffer, size, s_lastSetVersion);
}
// The string length including the zero terminator
*pSize = size;
}
调用者应调用该函数两次,一次将缓冲区地址设为 `null` 以确定所需的缓冲区大小,然后使用大小合适的缓冲区。
.DEF 文件内容如下
LIBRARY
EXPORTS
GetVersionBuffer
该函数导入到托管代码如下
[DllImport(DLL_LOCATION, CharSet = CharSet.Ansi)]
public static extern Boolean GetVersionBuffer
([MarshalAs(UnmanagedType.LPStr)] StringBuilder version, ref UInt32 size);
在 C# 代码中使用此函数很简单
UInt32 size = 0;
Model.ImportLibrary.GetVersionBuffer(null, ref size);
var sb = new StringBuilder((int)size);
Model.ImportLibrary.GetVersionBuffer(sb, ref size);
string version = sb.ToString();
上述代码确定了所需的缓冲区大小,然后检索版本 `string`。
传递字符串数组
使用 .NET 数组和 C++ `SAFEARRAY` 类型传递 `string` 数组非常容易。
以下 C++ 导出函数接受一个包含 `BSTR` 值数组的 `SAFEARRAY` 参数
extern void __stdcall SetStringArray(SAFEARRAY& safeArray)
{
if (safeArray.cDims == 1)
{
if ((safeArray.fFeatures & FADF_BSTR) == FADF_BSTR)
{
BSTR* bstrArray;
HRESULT hr = SafeArrayAccessData(&safeArray, (void**)&bstrArray);
long iMin = 0;
SafeArrayGetLBound(&safeArray, 1, &iMin);
long iMax = 0;
SafeArrayGetUBound(&safeArray, 1, &iMax);
for (long i = iMin; i <= iMax; ++i)
{
// Do something here with the data!
}
}
}
}
.DEF 文件内容如下
LIBRARY
EXPORTS
SetStringArray
该函数导入到托管代码如下
[DllImport(DLL_LOCATION, CharSet = CharSet.Ansi,
CallingConvention = CallingConvention.StdCall)]
public static extern void SetStringArray
([MarshalAs(UnmanagedType.SafeArray)] string[] array);
在 C# 代码中使用此函数很简单
string[] array = new string[4] {"one", "two", "three", "four"};
Model.ImportLibrary.SetStringArray(array);
尽管 C++ 代码有些混乱,但托管代码却简单得不能再简单了。
返回字符串数组
以下 C++ 导出函数使用 `BSTR` 值数组填充一个 `SAFEARRAY` 参数
extern void __stdcall GetStringArray(SAFEARRAY *&pSafeArray)
{
if (s_strings.size() > 0)
{
SAFEARRAYBOUND Bound;
Bound.lLbound = 0;
Bound.cElements = s_strings.size();
pSafeArray = SafeArrayCreate(VT_BSTR, 1, &Bound);
BSTR *pData;
HRESULT hr = SafeArrayAccessData(pSafeArray, (void **)&pData);
if (SUCCEEDED(hr))
{
for (DWORD i = 0; i < s_strings.size(); i++)
{
*pData++ = SysAllocString(s_strings[i].c_str());
}
SafeArrayUnaccessData(pSafeArray);
}
}
else
{
pSafeArray = nullptr;
}
}
假定 `s_strings` 变量是 `std::list
.DEF 文件内容如下
LIBRARY
EXPORTS
GetStringArray
该函数导入到托管代码如下
[DllImport(DLL_LOCATION, CharSet = CharSet.Ansi,
CallingConvention = CallingConvention.StdCall)]
public static extern void GetStringArray
([MarshalAs(UnmanagedType.SafeArray)] out string[] array);
这与 `SetStringArray` 方法几乎相同,只是参数声明为“`out`”参数。
可以从 C# 代码中像这样调用该函数
string[] array;
Model.ImportLibrary.GetStringArray(array);
与之前一样,C++ 代码有些混乱,但托管代码却简单得不能再简单了。
处理 ASCII 和 Unicode 字符串
通常,DLL 会为函数定义 ASCII 和 Unicode 版本。事实上,Microsoft 也这样做。非托管 `MessageBox` 函数实际上是在 Windows 头文件中这样定义的
#ifdef UNICODE
#define MessageBox MessageBoxW
#else
#define MessageBox MessageBoxA
#endif // !UNICODE
幸运的是,.NET Framework 内置了对此的支持。
我们可以这样定义第一个导出函数
extern void __stdcall SetVersionA(char *version)
{
// Store the version
}
extern void __stdcall SetVersionW(wchar_t *version)
{
// Store the version
}
.DEF 文件定义如下
LIBRARY
EXPORTS
SetVersionA
SetVersionW
该函数导入到 C# 托管代码如下
namespace DemoApp.Model
{
static class ImportLibrary
{
const String DLL_LOCATION = "DemoLibrary.dll";
[DllImport(DLL_LOCATION, CharSet = CharSet.Unicode,
CallingConvention = CallingConvention.StdCall)]
public static extern string SetVersion(string version);
}
}
请注意,函数名称被声明为 `SetVersion`,而不是 `SetVersionA` 或 `SetVersionW`,并且 `CharSet` 字段被设置为 `Unicode`。
在 C# 代码中使用此函数很简单
string version = "Version 3.4.5"
Model.ImportLibrary.SetVersion(version);
如果调试上述代码,您会看到调用的是 `SetVersionW` 导出。这是因为 `CharSet` 被设置为 `Unicode`。如果将 `CharSet` 改为 `Ansi`,然后进行调试,您会发现被调用的是 `SetVersionA` 导出!
我们可以使用 `ExactSpelling` 字段轻松禁用此功能,如下所示
namespace DemoApp.Model
{
static class ImportLibrary
{
const String DLL_LOCATION = "DemoLibrary.dll";
[DllImport(DLL_LOCATION, ExactSpelling = true,
CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
public static extern void SetVersion(string version);
}
}
现在 .NET 应用程序将尝试调用一个名为 `SetVersion` 的函数。由于不存在该函数,调用将失败。
结论
将 `string` 传递给非托管 C++ DLL 非常简单。返回 `string` 却没那么容易,并且容易出现内存泄漏和堆损坏等问题。一种简单的方法是让调用者分配所需大小的缓冲区。此方法适用于托管和非托管客户端。一个稍微简单的替代方法是使用 `BSTR *` 类型,但存在非托管客户端因未释放 `BSTR` 而引入内存泄漏的风险。
在托管应用程序和非托管 DLL 之间传递字符串数组也相对容易,尽管非托管 DLL 中的代码有些混乱。
我绝不是穷尽了在托管代码和非托管代码之间交换 `string` 的所有方法。其他方法留作读者练习。
示例代码
我创建了一个简单的 WPF 和 C++ DLL 应用程序,演示了本文讨论的思路。如果您不了解 WPF,请不要担心,您应该能够轻松理解相关的代码片段,并且幸运的话,您会受到启发去学习 WPF,我强烈推荐。
历史
- 2017年7月7日:初版