将指针传递给用C编写的DLL中的函数时的访问冲突读取位置

时间:2019-07-03 18:31:41

标签: c# c++

我正在尝试调用一个在方言之间转换sql查询的函数。我正在使用开源项目的DLL在C#大学项目中使用用于转换的函数。我面临的问题是我正在获取访问冲突读取位置。

我在这里阅读了一些有关堆栈溢出的文章,这表明在某处可能有一个错误的指针,但我找不到位置。我的指针没有损坏

转换功能是这样的:

       int ConvertSql(void *parser, const char *input, int64_t size, const char 
       **output, int64_t *out_size, int64_t *lines)
        {
            if(parser == NULL)
                return -1;

            SqlParser *sql_parser = (SqlParser*)parser;

            // Run conversion
            sql_parser->Convert(input, size, output, out_size, lines);

            return 0;
        }

我正在用C#调用该函数

                        char *parentInput;
                        fixed(char *input = &inputStr.ToCharArray()[0])
                        {
                            parentInput = input;
                        }

                        char** output = null;
                        Int64 out_size = 0;
                        Int64 lines = 0;
                        Int64 size = inputStr.Length;
                        Console.WriteLine(new IntPtr(&out_size)+"  "+ new IntPtr(&lines)+"  "+new IntPtr(&parserObj)+"   "+new IntPtr(output));
                        int result = ConvertSql(&parserObj, intputStr, size, output, &out_size, &lines);

我从这段代码中获取了我的解析器对象,该对象没有错误:

IntPtr parserObj = CreateParserObject();

该功能的dllimport使用以下代码:

[DllImport(dllName: "PATHTODLLFOLDER\\sqlparser.dll", EntryPoint = "CreateParserObject", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr CreateParserObject();
[DllImport(dllName: "PATHTODLLFOLDER\\sqlparser.dll", EntryPoint = "ConvertSql", CallingConvention = CallingConvention.Cdecl)]
public unsafe static extern int ConvertSql(void *parser, String input, Int64 size, char **output, Int64 *out_size, Int64 *lines);

1 个答案:

答案 0 :(得分:2)

在.NET中,通过P / invoke调用非托管方法(这是在调用extern方法时发生的情况)涉及对参数的各种类型转换,这称为“编组”,并自动完成在运行时的一部分称为“编组器”。

总的来说,编组指针是一个糟糕的主意。而是使用CLR编组器的功能,通过更改P /调用方法的签名为您将某些类型转换为指针:

// split on multiple lines for readability
[DllImport("PATHTODLLFOLDER\\sqlparser.dll", EntryPoint = "ConvertSql", CallingConvention = CallingConvention.Cdecl)]
public static extern int ConvertSql(
    IntPtr parser, 
    [MarshalAs(UnmanagedType.LPStr)] string input,
    long size, 
    out IntPtr output, // see explanation below
    out long out_size, 
    out long lines);

关于以上几点。首先,我自由使用了C#类型别名(字符串,长整数),因为它是C#的惯用语,但不会改变行为。另外,由于不再有指针,因此不需要unsafe

首先,我将parser声明为IntPtr,因为如果需要,它们会自动转换为void*,而CreateParserObject()早已返回了该值。

第二个out参数可以转换为指向未初始化对象的指针,因此通过将out_sizelines都标记为out,可以解决另一个问题。

input是一个字符串,在.NET中具有特定格式。由于您的函数采用const char*,因此您需要告诉编组器如何转换字符。这就是MarshalAs属性出现的地方:默认转换对您不起作用。在这种情况下,UnmanagedType.LPStr的意思是char*,因此字符串将被转换。运行时将为您管理内存。

但是在这里,我们遇到了一个大障碍:输出。在整理事物时,总是存在关于生命周期的问题,或者特别是:谁释放了记忆? outputchar**的事实意味着解析器分配一个内存块,然后通过该内存块将其返回,这意味着调用者必须释放它。从一开始,C ++设计就很糟糕,因为调用者不知道如何分配内存。是malloc吗? new[]?特定于平台的API,例如LocalAlloc?它是指向静态内存块的指针吗?通常,这些API附带了文档,这些文档准确地告诉了您完成使用指针后的处理​​方式。好的C ++ API返回智能指针,或要求调用者传递先前分配的内存块,然后供他们使用。

但是,这就是您在玩的东西,因此,这是使其工作的方法。首先,您认为可以将output声明为[MarshalAs(UnmanagedType.LPStr)] out string:编组将把字符复制到托管字符串中并返回它……但是然后是本机字符串(在C ++端)的内存将泄漏,因为运行时不知道如何分配字符串,因此它不愿对此做任何事情。此外,这还假定该字符串以null结尾,但这并非总是如此。

因此,另一种选择是将output声明为out IntPtr。然后,您可以使用Marshal.PtrToStringAnsi将指针转换为字符串,然后释放它...但是您需要首先知道如何分配指针。

将它们放在一起:

var parserObj = CreateParserObject();

var output = IntPtr.Zero;
try
{
    long lines;
    long out_size;
    int result = ConvertSql(parserObj, inputStr, inputStr.Length, out output, out out_size, out lines);

    var outputStr = Marshal.PtrToStringAnsi(output, (int)out_size);
    // do what you want with outputStr here
}
finally
{
    if (output != IntPtr.Zero)
    {
        // release output here
    }
}

哦,还有一个最后的想法:CreateParserObject()返回的任何内容都可能必须在某一时刻被释放。您可能需要另一个功能,例如:

[DllImport(/* ... */)]
public static extern void DestroyParserObject(IntPtr parserObject);

它甚至可能已经存在于您的DLL中。

祝你好运!