【问题标题】:C# call C++ DLL passing pointer-to-pointer argumentC# 调用 C++ DLL 传递指针到指针参数
【发布时间】:2013-12-06 08:34:44
【问题描述】:

你们能帮我解决以下问题吗? 我有一个 C++ 函数 dll,它将被另一个 C# 应用程序调用。 我需要的功能之一如下:

struct DataStruct
{
    unsigned char* data;
    int len;
};

DLLAPI int API_ReadFile(const wchar_t* filename, DataStruct** outData);

我用 C# 编写了以下代码:

class CS_DataStruct
{
    public byte[] data;
    public int len;
}

[DllImport("ReadFile.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
private static extern int API_ReadFile([MarshalAs(UnmanagedType.LPWStr)]string filename, ref CS_DataStruct data);

不幸的是,上面的代码不起作用......我猜这是由于 C++ func 需要一个指向 DataStruct 的指针,而我只是传递了一个 CS_DataStruct 的引用。

我可以知道如何将指向指针的指针传递给 C++ 函数吗?如果不可能,是否有任何解决方法? (C++ API 是固定的,所以不能把 API 改成指针)

编辑: DataStruct 的内存将由 c++ 函数分配。在此之前,我不知道数据数组应该有多大。 (感谢下面的cmets)

【问题讨论】:

  • 传递指向指针的指针意味着本机函数分配一些内存并传回指向它的指针。我看不出这在托管代码中如何适用。
  • 也许你应该进一步澄清。 C++ 代码会为数据分配内存吗?谁将负责释放内存?您可能还想查阅有关阵列编组的 Microsoft 文档:msdn.microsoft.com/en-us/library/z6cfh6e6%28v=vs.110%29.aspx。我也认为你应该在 C# 中使用 struct 而不是 class。 struct 是值类型,而 class 是引用类型。
  • 如何解除分配?而且我仍然看不到 DataStruct** 的原因。即使仅在 C++ 中,我也会假设调用者将在本地定义一个 DataStruct 并传递它的指针。无需让 API_ReadFile 为结构分配内存。
  • 等等,这不是 DataStruct 的数组吗?如果是这样,您可能别无选择,只能使用 unsafe 代码或 C++/CLI。您可以访问 API_ReadFile 的源代码吗?还是在 C++ 中使用的示例代码?
  • @Yongwei Wu,该库还提供了免费DataStruct的API,抱歉没有在问题中提及。我认为如果最后一个参数更改为“DataStruct*& outData”会更清楚。但无论如何,这就是 API,我无法更改

标签: c# c++ dll


【解决方案1】:

我使用了以下测试实现:

int API_ReadFile(const wchar_t* filename, DataStruct** outData)
{
    *outData = new DataStruct();
    (*outData)->data = (unsigned char*)_strdup("hello");
    (*outData)->len = 5;
    return 0;
}

void API_Free(DataStruct** pp)
{
    free((*pp)->data);
    delete *pp;
    *pp = NULL;
}

访问这些函数的 C# 代码如下:

[StructLayout(LayoutKind.Sequential)]
struct DataStruct
{
    public IntPtr data;
    public int len;
};

[DllImport("ReadFile.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
unsafe private static extern int API_ReadFile([MarshalAs(UnmanagedType.LPWStr)]string filename, DataStruct** outData);

[DllImport("ReadFile.dll", CallingConvention = CallingConvention.Cdecl)]
unsafe private static extern void API_Free(DataStruct** handle);

unsafe static int ReadFile(string filename, out byte[] buffer)
{
    DataStruct* outData;
    int result = API_ReadFile(filename, &outData);
    buffer = new byte[outData->len];
    Marshal.Copy((IntPtr)outData->data, buffer, 0, outData->len);
    API_Free(&outData);
    return result;
}

static void Main(string[] args)
{
    byte[] buffer;
    ReadFile("test.txt", out buffer);
    foreach (byte ch in buffer)
    {
        Console.Write("{0} ", ch);
    }
    Console.Write("\n");
}

数据现在安全地传输到buffer,应该没有内存泄漏。我希望它会有所帮助。

【讨论】:

  • 吴永伟 - 这里有一个问题,既然 Charset 是 Unicode,单个 char 不一定是一个字节的大小,对吗?
  • char 的大小始终是byte 的大小。 Charset=Unicode 意味着 System.String 默认编组为 wchar_t*(而不是 char*)。实际上 Charset=Unicode 在这里应该没有效果,因为字符串被显式编组为 UnmanagedType.LPWStr。
【解决方案2】:

不必使用unsafe 将指针从DLL 传递到数组。这是一个示例(请参阅“结果”参数)。关键是使用ref 属性。它还展示了如何传递其他几种类型的数据。

在 C++/C 中定义:

#ifdef __cplusplus
extern "C" {
#endif

#ifdef BUILDING_DLL
#define DLLCALL __declspec(dllexport)
#else
#define DLLCALL __declspec(dllimport)
#endif

static const int DataLength = 10;
static const int StrLen = 16;
static const int MaxResults = 30;

enum Status { on = 0, off = 1 };

struct Result {
    char name[StrLen]; //!< Up to StrLen-1 char null-terminated name
    float location;  
    Status status;
};

/**
* Analyze Data
* @param data [in] array of doubles
* @param dataLength [in] number of floats in data
* @param weight [in]
* @param status [in] enum with data status
* @param results  [out] array of MaxResults (pre-allocated) DLLResult structs.
*                    Up to MaxResults results will be returned.
* @param nResults  [out] the actual number of results being returned.
*/
void DLLCALL __stdcall analyzeData(
      const double *data, int dataLength, float weight, Status status, Result **results, int *nResults);

#ifdef __cplusplus
}
#endif

在 C# 中使用:

private const int DataLength = 10;
private const int StrLen = 16;
private const int MaxThreatPeaks = 30;

public enum Status { on = 0, off = 1 };

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct Result
{
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = StrLen)] public string name; //!< Up to StrLen-1 char null-terminated name 
    public float location;  
    public Status status;       
}

[DllImport("dllname.dll", CallingConvention = CallingConvention.StdCall, EntryPoint = "analyzeData@32")] // "@32" is only used in the 32-bit version.
public static extern void analyzeData(
    double[] data,
    int dataLength, 
    float weight, 
    Status status,
    [MarshalAs(UnmanagedType.LPArray, SizeConst = MaxResults)] ref Result[] results, 
    out int nResults
);

如果没有extern "C" 部分,C++ 编译器会以编译器依赖的方式破坏导出名称。我注意到 EntryPoint / Exported 函数名称与 64 位 DLL 中的函数名称完全匹配,但在编译成 32 位 DLL 时附加了一个“@32”(数字可能会有所不同)。运行dumpbin /exports dllname.dll 确定导出的名称。在某些情况下,您可能还需要使用 DLLImport 参数 ExactSpelling = true。请注意,此函数声明为__stdcall。如果未指定,则为__cdecl,您需要CallingConvention.Cdecl

这是它在 C# 中的使用方式:

Status status = Status.on;
double[] data = { -0.034, -0.05, -0.039, -0.034, -0.057, -0.084, -0.105, -0.146, -0.174, -0.167};
Result[] results = new Result[MaxResults];
int nResults = -1; // just to see that it changes (input value is ignored)
analyzeData(data, DataLength, 1.0f, status, ref results, out nResults);

【讨论】:

    【解决方案3】:

    如果您确实调用了本机代码,请确保您的结构在内存中对齐。 CLR 不保证对齐,除非你推动它。

    试试

    [StructLayout(LayoutKind.Explicit)]
    struct DataStruct
    {
        string data;
        int len;
    };
    

    更多信息: http://www.developerfusion.com/article/84519/mastering-structs-in-c/

    【讨论】:

    • 感谢您的回复,但它不起作用。当 LayoutKind 更改为 Explicit 时,编译器会说“标有 StructLayout(LayoutKind.Explicit) 的'DataStruct.data' 实例字段类型必须具有 FieldOffset 属性”
    猜你喜欢
    • 2020-08-19
    • 2018-12-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多