Passing LPSTR between Win32 DLL and C# using MethodInfo.Invoke
I am working on a project that needs to be able to call functions in Win32 DLLs. However, the name of the DLL, function, and data types of all arguments and return type are not known at compile time, so using DLLImport is not an option. Basically, this routine could feasibly call any function in any DLL and pass in any arguments. After a lot of searching, I have been able to successfully put together some code that can do this, including passing numerics and strings into the function and even passing numeric arguments by reference. However, I have hit a brick wall trying to pass strings back from the DLL.
To simplify testing, I used Visual Studio 6 to compile a simple Win32 DLL called PassCharPtr.dll containing one function as follows:
PassCharPtr.def file:
EXPORTS
TestPassCharPtr
PassCharPtr.h file:
#include <windows.h>
extern "C" int __stdcall TestPassCharPtr(LPSTR, LONG);
PassCharPtr.cpp file:
#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;
}
To test my DLL, I created a simple VB6 application:
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
Everything is working great. Now, here's my C# test project in VS2010 that attempts to access this function:
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));
}
开发者_StackOverflow社区 }
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 "";
}
}
}
If I declare the first argument as char[] (System.String), I can successfully pass a string into the function, but it generates an exception (accessing protected memory) when the DLL attempts to replace that string with the string to return.
If I declare the first argument as LPSTR (System.IntPtr), I cannot pass a string into the function. However, upon returning from the call, argValuesArray[0] contains what appears to be an address. I have not been able to figure out how to convert that address into the returned string yet. I have tried using String mvValue = Marshal.PtrToStringAnsi((IntPtr)argValuesArray[0]), but this returns an empty string.
There are still a lot of holes in this code, but I hope the general idea is clear enough. Can anyone tell me what data type the first argument should be declared as to be able to pass strings both into and out of this function successfully and how to do any necessary conversions on that data type?
LPSTR is generally marshalled as StringBuilder.
精彩评论