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

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (28投票s)

2017年7月7日

CPOL

6分钟阅读

viewsIcon

89600

downloadIcon

1213

如何将字符串传递给托管和非托管代码之间

背景

读者应具备 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日:初版
© . All rights reserved.