Передача LPSTR между Win32 DLL и C# с помощью MethodInfo.Invoke

Я работаю над проектом, который должен иметь возможность вызывать функции в библиотеках Win32. Однако имя библиотеки DLL, функция и типы данных всех аргументов и тип возвращаемого значения неизвестны во время компиляции, поэтому использование DLLImport не является вариантом. По сути, эта подпрограмма может вызывать любую функцию в любой DLL и передавать любые аргументы. После долгих поисков мне удалось успешно собрать некоторый код, который может это сделать, включая передачу числовых и строковых значений в функцию и даже передачу числовых аргументов по ссылке. Однако я наткнулся на кирпичную стену, пытаясь передать строки обратно из DLL.

Чтобы упростить тестирование, я использовал Visual Studio 6 для компиляции простой Win32-библиотеки DLL с именем PassCharPtr.dll, содержащей одну следующую функцию:

Файл PassCharPtr.def:

   EXPORTS
       TestPassCharPtr

Файл PassCharPtr.h:

#include <windows.h>
extern "C" int __stdcall TestPassCharPtr(LPSTR, LONG);

Файл PassCharPtr.cpp:

#include "PassCharPtr.h"

int WINAPI DllEntryPoint(HINSTANCE hinst,
                         unsigned long reason,
                         void*)
{
    return 1;
}


/*----------------------------------------------------------------*/
int __stdcall TestPassCharPtr(LPSTR szString, LONG cSize)
{
    MessageBox(0, szString, "Inside PassCharPtr.dll", 0);

    char buffer[] = "abcdefghijklmnopqrstuvwxyz";

    if (cSize > strlen(buffer))
    {
        strcpy(szString,buffer);
        return strlen(buffer);
    }
    return -cSize;
}

Чтобы протестировать свою DLL, я создал простое приложение VB6:

Private Declare Function TestPassCharPtr Lib "PassCharPtr" (ByVal buffer As String, ByVal lSize As Long) As Long

Private Sub btnTest_Click()
Dim sBuffer As String
Dim lResult As Long

    sBuffer = "This is a very long string!!!!!!!!!"
    lResult = TestPassCharPtr(sBuffer, Len(sBuffer))

Debug.Print "Result: "; lResult
Debug.Print "Buffer: "; Left(sBuffer, lResult)

End Sub

Все работает отлично. Теперь вот мой тестовый проект C# в VS2010, который пытается получить доступ к этой функции:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.InteropServices;

namespace TestPassCharPtr
{
    class Program
    {
        /// <summary>
        /// Define DLL and function to call. Setup data types for return value and arguments
        /// and setup values to pass into function.
        /// 
        /// All data types should be declared using C\C++ names to facilitate using
        /// existing Win32 API documentation to define function calls.
        /// 
        /// When passing a string to a function call, enclose the string in quotes ("")
        /// 
        /// </summary>
        /// <param name="args">Unused</param>
        static void Main(string[] args)
        {
            string fileName = "PassCharPtr.dll";
            string funcName = "TestPassCharPtr";

            string returnType = "int";

            // comma-delimited list of argument data types
            // using this declaration successfully passes string in
            // but generates exception when passing string back!
            string argTypesList = "char[], int";  

            // using this declaration fails to pass string in, but does
            // not generate an exception when passing string back!
            //string argTypesList = "LPSTR, int";              

            // comma-delimited list of argument values  
            string argValuesList = "\"This is a very long string!!!!\", 30";   

            TestDLLFunction(fileName, funcName, returnType, argTypesList, argValuesList);
            MessageBox.Show("Done");

        }

        /// <summary>
        /// Calls a DLL function.
        /// 
        /// Reference: http://www.pcreview.co.uk/forums/calling-native-c-function-using-definepinvokemethod-and-returning-calculated-value-pointer-reference-t2329473.html
        /// 
        /// </summary>
        /// <param name="dllFilename">Filename of DLL (excluding path!)</param>
        /// <param name="entryPoint">Function name</param>
        /// <param name="retType">Return value data type</param>
        /// <param name="argTypesList">Comma-delimited list of argument data types</param>
        /// <param name="argValuesList">Comma-delimited list of argument values</param>
        private static void TestDLLFunction(string dllPath, string entryPoint, string retType, string argTypesList, string argValuesList)
        {
            Type returnType = null;
            Type[] argTypesArray = null;
            object[] argValuesArray = null;
            object returnValue = null;

            // get return data type
            returnType = Type.GetType(ConvertToNetType(retType));

            // get argument data types for the function to call
            string[] argTypes = argTypesList.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);

            if (argTypes.Length > 0)
            {
                // create a list of data types for each argument
                List<Type> listArgTypes = new List<Type>();
                foreach (var argType in argTypes)
                {
                    string netType = ConvertToNetType(argType);
                    string byRef = IsPointer(argType) ? "&" : "";

                    listArgTypes.Add(Type.GetType(netType + byRef));
                }
                // convert the list to an array
                argTypesArray = listArgTypes.ToArray<Type>();

                // get values to pass as arguments to the function
                string[] argValues = argValuesList.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);

                // remove quotes from strings
                for (int i = 0; i < argValues.Length; i++)
                {
                    argValues[i] = argValues[i].Replace("\"", "").Trim();
                }
                argValuesArray = argValues.ToArray<object>();

                // verify argument data types count and argument values count match!
                if (argValuesArray.Length != argTypesArray.Length)
                {
                    Console.WriteLine(string.Format("The number of parameter types ({0}) does not match the number of parameter values ({1}).", argTypesArray.Length, argValuesArray.Length));
                    return;
                }

                // convert all argument values to the proper data types
                for (int i = 0; i < argValuesArray.Length; i++)
                {
                    if (argTypesArray[i] == Type.GetType("System.IntPtr&"))
                    {
                        argValuesArray[i] = (IntPtr)0;
                    }
                    else
                    {
                        argValuesArray[i] = ConvertParameter(argValuesArray[i], argTypesArray[i]);
                    }
                }
            }
            else
            {
                argTypesArray = null;
                argValuesArray = null;
            }

            // Create a dynamic assembly and a dynamic module
            AssemblyName assemblyName = new AssemblyName();
            assemblyName.Name = dllPath;

            AssemblyBuilder dynamicAssembly = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);

            ModuleBuilder dynamicModule = dynamicAssembly.DefineDynamicModule("tempModule");

            // Dynamically construct a global PInvoke signature using the input information
            MethodBuilder dynamicMethod = dynamicModule.DefinePInvokeMethod(entryPoint, dllPath,
                MethodAttributes.Static | MethodAttributes.Public | MethodAttributes.PinvokeImpl,
                CallingConventions.Standard, returnType, argTypesArray, CallingConvention.Winapi, CharSet.Ansi);

            // Add PreserveSig to the method implementation flags. NOTE: If this line
            // is commented out, the return value will be zero when the method is invoked.
            dynamicMethod.SetImplementationFlags(dynamicMethod.GetMethodImplementationFlags() | MethodImplAttributes.PreserveSig);

            // This global method is now complete
            dynamicModule.CreateGlobalFunctions();

            // Get a MethodInfo for the PInvoke method
            MethodInfo mi = dynamicModule.GetMethod(entryPoint);

            // Invoke the function
            try
            {
                returnValue = mi.Invoke(null, argValuesArray);
            }
            catch (Exception ex)
            {
                Console.WriteLine(string.Format("Error: {0}", ex.Message));
                if (ex.InnerException != null)
                {
                    Console.WriteLine(string.Format("  Error: {0}", ex.InnerException.Message));
                }
            }

            if (returnValue != null)
            {
                Console.WriteLine(string.Format("Return value: {0}", returnValue.ToString()));
            }

            if (argValuesArray != null)
            {
                for (int i = 0; i < argValuesArray.Length; i++)
                {
                    if (argValuesArray[i] != null)
                    {
                        Console.WriteLine(string.Format("Argument {0}: {1}", i, argValuesArray[i].ToString()));
                    }
                }
            }
        }

        /// <summary>
        /// Converts a string to another data type.
        /// </summary>
        /// <param name="value">Value to be converted</param>
        /// <param name="dataType">Data type to convert to</param>
        /// <returns>Converted value</returns>
        private static object ConvertParameter(object value, Type dataType)
        {
            // determine the base data type (remove "&" from end of "by reference" data types)
            string baseDataType = dataType.ToString();
            if (baseDataType.EndsWith("&"))
            {
                baseDataType = baseDataType.Substring(0, baseDataType.Length - 1);
            }
            return Convert.ChangeType(value, Type.GetType(baseDataType));
        }

        /// <summary>
        /// Determines whether the indicated native data type is a pointer
        /// </summary>
        /// <param name="dataType">Native (unmanaged) data type</param>
        /// <returns>true if data type is a pointer; false otherwise</returns>
        private static bool IsPointer(string dataType)
        {
            string lowerDataType = dataType.ToLower();
            if (lowerDataType.StartsWith("lp") || lowerDataType.EndsWith("*"))
            {
                return true;
            }
            return false;
        }

        /// <summary>
        /// Convert unmanaged data type names to .NET data type names
        ///
        /// (for simplicity, all types unused by this example were removed)
        ///
        /// </summary>
        /// <param name="type">Unmanaged data type name</param>
        /// <returns>Corresponding .NET data type name</returns>
        private static string ConvertToNetType(string type)
        {
            string lowerType = type.ToLower();

            if (lowerType.Contains("int"))
            {
                return "System.Int32";
            }
            else if (lowerType.Contains("lpstr"))
            {
                return "System.IntPtr";
            }
            else if (lowerType.Contains("char[]"))
            {
                return "System.String";
            }
            return "";
        }

    }
}

Если я объявляю первый аргумент как char[] (System.String), я могу успешно передать строку в функцию, но она генерирует исключение (доступ к защищенной памяти), когда DLL пытается заменить эту строку возвращаемой строкой.

Если я объявлю первый аргумент как LPSTR (System.IntPtr), я не смогу передать строку в функцию. Однако по возвращении из вызова argValuesArray[0] содержит нечто похожее на адрес. Я еще не смог понять, как преобразовать этот адрес в возвращаемую строку. Я попытался использовать String mvValue = Marshal.PtrToStringAnsi((IntPtr)argValuesArray[0]), но это возвращает пустую строку.

В этом коде еще много дыр, но я надеюсь, что общая идея достаточно ясна. Может ли кто-нибудь сказать мне, какой тип данных должен быть объявлен первым аргументом, чтобы иметь возможность успешно передавать строки как в эту функцию, так и из нее, и как выполнять любые необходимые преобразования для этого типа данных?


person acordner    schedule 22.04.2011    source источник
comment
Acordner. Пожалуйста, не используйте код C#, который вы разместили, он слишком усложняет такой простой фрагмент кода (около 2 строк) в 150, я бы оценил.   -  person Security Hound    schedule 22.04.2011


Ответы (1)


LPSTR обычно маршалируется как StringBuilder.

person user703016    schedule 22.04.2011
comment
Я пробовал это. Проблема в том, что Type.GetType("System.StringBuilder") возвращает null. Поэтому я не могу создать аргумент argTypesArray для передачи сюда: MethodBuilder dynamicMethod = dynamicModule.DefinePInvokeMethod(entryPoint, dllFilename, MethodAttributes.Static | MethodAttributes.Public | MethodAttributes. CharSet.Ansi); - person acordner; 22.04.2011
comment
Я искал способ использовать MarshalAs с DefinePInvokeMethod(), но я не смог понять, как/если это можно сделать. - person acordner; 22.04.2011
comment
Внеся несколько небольших изменений в поддержку использования StringBuilder, я смог заставить это работать! Вот изменения (в отдельных комментариях): Создание массива типов данных для аргументов: // создаем список типов данных для каждого аргумента List‹Type› listArgTypes = new List‹Type›(); foreach (var argType в argTypes) { listArgTypes.Add(Type.GetType(ConvertToNetType(argType))); } // преобразовать список в массив argTypesArray = listArgTypes.ToArray‹Type›(); - person acordner; 22.04.2011
comment
Преобразование значений аргументов в надлежащие типы данных: // преобразование всех значений аргументов в надлежащие типы данных Console.WriteLine(Преобразование значений аргументов в надлежащие типы данных...); for (int i = 0; i ‹ ArgValuesArray.Length; i++) { argValuesArray[i] = ConvertParameter(argValuesArray[i], argTypesArray[i]); } - person acordner; 22.04.2011
comment
Преобразование имен неуправляемых типов данных в имена управляемых типов данных: private static string ConvertToNetType(string type) { string lowerType = type.ToLower().Trim(); строка byRef = lowerType.EndsWith(*) ? & : ; if (lowerType.Equals(int)) { return System.Int32 + byRef; } else if (lowerType.Contains(lpstr)) { return System.Text.StringBuilder; } возвращение ; } - person acordner; 22.04.2011
comment
Выглядит некрасиво, но информация есть. Я также удалил метод IsPointer(). Спасибо за помощь Heandel, Кэмерон и Аарон! - person acordner; 22.04.2011