2010-11-12 24 views
24

EDITAR: He publicado una mejor implementación de esto, a continuación. Dejé esto aquí para que las respuestas tuvieran sentido.Llamar a una DLL de Delphi desde una aplicación de C# .NET

He realizado numerosas búsquedas del método correcto para escribir una DLL en Delphi y poder llamarla desde C#, pasando y devolviendo cadenas. Mucha de la información era incompleta o incorrecta. Después de mucho ensayo y error, encontré la solución.

Esto fue compilado usando Delphi 2007 y VS 2010. Sospecho que funcionará bien en otras versiones también.

Aquí está el código Delphi. Recuerde incluir información de versión en el proyecto.

library DelphiLibrary; 

uses SysUtils; 

// Compiled using Delphi 2007. 

// NOTE: If your project doesn't have version information included, you may 
// receive the error "The "ResolveManifestFiles" task failed unexpectedly" 
// when compiling the C# application. 

{$R *.res} 

// Example function takes an input integer and input string, and returns 
// inputInt + 1, and inputString + ' ' + IntToStr(outputInt) as output 
// parameters. If successful, the return result is nil (null), otherwise it is 
// the exception message string. 


// NOTE: I've posted a better version of this below. You should use that instead. 

function DelphiFunction(inputInt : integer; inputString : PAnsiChar; 
         out outputInt : integer; out outputString : PAnsiChar) 
         : PAnsiChar; stdcall; export; 
var s : string; 
begin 
    outputInt := 0; 
    outputString := nil; 
    try 
    outputInt := inputInt + 1; 
    s := inputString + ' ' + IntToStr(outputInt); 
    outputString := PAnsiChar(s); 
    Result := nil; 
    except 
    on e : exception do Result := PAnsiChar(e.Message); 
    end; 
end; 

// I would have thought having "export" at the end of the function declartion 
// (above) would have been enough to export the function, but I couldn't get it 
// to work without this line also. 
exports DelphiFunction; 

begin 
end. 

Aquí está el código C#:

using System; 
using System.Runtime.InteropServices; 

namespace CsharpApp 
{ 
    class Program 
    { 
     // I added DelphiLibrary.dll to my project (NOT in References, but 
     // "Add existing file"). In Properties for the dll, I set "BuildAction" 
     // to None, and "Copy to Output Directory" to "Copy always". 
     // Make sure your Delphi dll has version information included. 

     [DllImport("DelphiLibrary.dll", 
        CallingConvention = CallingConvention.StdCall, 
        CharSet = CharSet.Ansi)] 
     public static extern 
      string DelphiFunction(int inputInt, string inputString, 
            out int outputInt, out string outputString); 

     static void Main(string[] args) 
     { 
      int inputInt = 1; 
      string inputString = "This is a test"; 
      int outputInt; 
      string outputString; 


// NOTE: I've posted a better version of this below. You should use that instead. 


      Console.WriteLine("inputInt = {0}, intputString = \"{1}\"", 
           inputInt, inputString); 
      var errorString = DelphiFunction(inputInt, inputString, 
              out outputInt, out outputString); 
      if (errorString != null) 
       Console.WriteLine("Error = \"{0}\"", errorString); 
      else 
       Console.WriteLine("outputInt = {0}, outputString = \"{1}\"", 
            outputInt, outputString); 
      Console.Write("Press Enter:"); 
      Console.ReadLine(); 
     } 
    } 
} 

Espero que esta información ayuda a otra persona para no tener que tirar de los pelos tanto como lo hice.

+0

No es realmente una pregunta, pero 1 :). –

+0

No estoy familiarizado con Delphi, pero sé si es posible convertirlo en 'COM' es fácil de usar en C#, hice una pequeña búsqueda y encontré algún recurso que trata acerca de delphi y la relación COM aquí: http: // delphi.about.com/library/weekly/aa122804a.htm –

+5

Debe volver a formular su pregunta para decir "¿Cuál es la forma correcta de utilizar un DLL de Delphi desde una aplicación C# .NET?" y luego respondete a ti mismo con el resto de tu publicación. Consulte http://stackoverflow.com/faq (Puede contestar su propia pregunta) y aquí: http://meta.stackexchange.com/questions/12513/should-i-not-answer-my-own-questions –

Respuesta

5

Como dijo Jeroen Pluimers en su comentario, debe tener en cuenta que las cadenas de Delphi se cuentan por referencia.

IMO, en circunstancias tales que se supone que debe devolver una cadena en entornos heterogéneos, debe pedirle a la persona que llama que proporcione un búfer para el resultado, y la función debe llenar ese búfer. De esta manera, la persona que llama es responsable de crear el búfer y eliminarlo cuando termina. Si echas un vistazo a las funciones de la API de Win32, verás que hacen lo mismo cuando necesitan devolver una cadena a la persona que llama.

Para hacerlo, puede usar PChar (ya sea PAnsiChar o PWideChar) como el tipo de parámetro de función, pero también debe pedirle a la persona que llama que proporcione el tamaño del búfer. Echar un vistazo a mi respuesta en el siguiente enlace, para un código fuente de ejemplo:

Exchanging strings (PChar) between a Freepascal compiled DLL and a Delphi compiled EXE

La pregunta es específicamente sobre el intercambio de cuerda entre FreePascal y Delphi, pero la idea y la respuesta es aplicable a su caso también .

+0

Gracias a ambos por señalar esto: es el tipo de error potencial que podría llevar una eternidad. En el fondo de mi mente me preguntaba sobre esto, pero no presté suficiente atención a esa pequeña voz de la razón. Me avergüenza. ; p He publicado un ejemplo mejor a continuación, que soluciona este problema. –

+0

En mi prueba, he encontrado que al devolver el resultado de 'StrNew (PChar (s))' no hay error como 'GetMem' ->' StrPLCopy', ¿es esto más seguro? –

22

En base a las respuestas a mi publicación, he creado un nuevo ejemplo que utiliza almacenamientos intermedios de cadena para las cadenas devueltas, en lugar de solo devolver PAnsiChars.

Delphi DLL fuente:

library DelphiLibrary; 

uses SysUtils; 

// Compiled using Delphi 2007. 

// NOTE: If your project doesn't have version information included, you may 
// receive the error "The "ResolveManifestFiles" task failed unexpectedly" 
// when compiling the C# application. 

{$R *.res} 

// A note on returing strings. I had originally written this so that the 
// output string was just a PAnsiChar. But several people pointed out that 
// since Delphi strings are reference-counted, this was a bad idea since the 
// memory for the string could get overwritten before it was used. 
// 
// Because of this, I re-wrote the example so that you have to pass a buffer for 
// the result strings. I saw some examples of how to do this, where they 
// returned the actual string length also. This isn't necessary, because the 
// string is null-terminated, and in fact the examples themselves never used the 
// returned string length. 


// Example function takes an input integer and input string, and returns 
// inputInt + 1, and inputString + ' ' + IntToStr(outputInt). If successful, 
// the return result is true, otherwise errorMsgBuffer contains the the 
// exception message string. 
function DelphiFunction(inputInt : integer; 
         inputString : PAnsiChar; 
         out outputInt : integer; 
         outputStringBufferSize : integer; 
         var outputStringBuffer : PAnsiChar; 
         errorMsgBufferSize : integer; 
         var errorMsgBuffer : PAnsiChar) 
         : WordBool; stdcall; export; 
var s : string; 
begin 
    outputInt := 0; 
    try 
    outputInt := inputInt + 1; 
    s := inputString + ' ' + IntToStr(outputInt); 
    StrLCopy(outputStringBuffer, PAnsiChar(s), outputStringBufferSize-1); 
    errorMsgBuffer[0] := #0; 
    Result := true; 
    except 
    on e : exception do 
    begin 
     StrLCopy(errorMsgBuffer, PAnsiChar(e.Message), errorMsgBufferSize-1); 
     Result := false; 
    end; 
    end; 
end; 

// I would have thought having "export" at the end of the function declartion 
// (above) would have been enough to export the function, but I couldn't get it 
// to work without this line also. 
exports DelphiFunction; 

begin 
end. 

C# Código:

using System; 
using System.Runtime.InteropServices; 

namespace CsharpApp 
{ 
    class Program 
    { 
     // I added DelphiLibrary.dll to my project (NOT in References, but 
     // "Add existing file"). In Properties for the dll, I set "BuildAction" 
     // to None, and "Copy to Output Directory" to "Copy always". 
     // Make sure your Delphi dll has version information included. 

     [DllImport("DelphiLibrary.dll", 
        CallingConvention = CallingConvention.StdCall, 
        CharSet = CharSet.Ansi)] 
     public static extern bool 
      DelphiFunction(int inputInt, string inputString, 
          out int outputInt, 
          int outputStringBufferSize, ref string outputStringBuffer, 
          int errorMsgBufferSize, ref string errorMsgBuffer); 

     static void Main(string[] args) 
     { 
      int inputInt = 1; 
      string inputString = "This is a test"; 
      int outputInt; 
      const int stringBufferSize = 1024; 
      var outputStringBuffer = new String('\x00', stringBufferSize); 
      var errorMsgBuffer = new String('\x00', stringBufferSize); 

      if (!DelphiFunction(inputInt, inputString, 
           out outputInt, 
           stringBufferSize, ref outputStringBuffer, 
           stringBufferSize, ref errorMsgBuffer)) 
       Console.WriteLine("Error = \"{0}\"", errorMsgBuffer); 
      else 
       Console.WriteLine("outputInt = {0}, outputString = \"{1}\"", 
            outputInt, outputStringBuffer); 

      Console.Write("Press Enter:"); 
      Console.ReadLine(); 
     } 
    } 
} 

Y aquí está una clase adicional que muestra cómo cargar el archivo DLL de forma dinámica (lo siento por las largas líneas):

using System; 
using System.Runtime.InteropServices; 

namespace CsharpApp 
{ 
    static class DynamicLinking 
    { 
     [DllImport("kernel32.dll", EntryPoint = "LoadLibrary")] 
     static extern int LoadLibrary([MarshalAs(UnmanagedType.LPStr)] string lpLibFileName); 

     [DllImport("kernel32.dll", EntryPoint = "GetProcAddress")] 
     static extern IntPtr GetProcAddress(int hModule, [MarshalAs(UnmanagedType.LPStr)] string lpProcName); 

     [DllImport("kernel32.dll", EntryPoint = "FreeLibrary")] 
     static extern bool FreeLibrary(int hModule); 

     [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Ansi)] 
     delegate bool DelphiFunction(int inputInt, string inputString, 
            out int outputInt, 
            int outputStringBufferSize, ref string outputStringBuffer, 
            int errorMsgBufferSize, ref string errorMsgBuffer); 

     public static void CallDelphiFunction(int inputInt, string inputString, 
               out int outputInt, out string outputString) 
     { 
      const string dllName = "DelphiLib.dll"; 
      const string functionName = "DelphiFunction"; 

      int libHandle = LoadLibrary(dllName); 
      if (libHandle == 0) 
       throw new Exception(string.Format("Could not load library \"{0}\"", dllName)); 
      try 
      { 
       var delphiFunctionAddress = GetProcAddress(libHandle, functionName); 
       if (delphiFunctionAddress == IntPtr.Zero) 
        throw new Exception(string.Format("Can't find function \"{0}\" in library \"{1}\"", functionName, dllName)); 

       var delphiFunction = (DelphiFunction)Marshal.GetDelegateForFunctionPointer(delphiFunctionAddress, typeof(DelphiFunction)); 

       const int stringBufferSize = 1024; 
       var outputStringBuffer = new String('\x00', stringBufferSize); 
       var errorMsgBuffer = new String('\x00', stringBufferSize); 

       if (!delphiFunction(inputInt, inputString, out outputInt, 
            stringBufferSize, ref outputStringBuffer, 
            stringBufferSize, ref errorMsgBuffer)) 
        throw new Exception(errorMsgBuffer); 

       outputString = outputStringBuffer; 
      } 
      finally 
      { 
       FreeLibrary(libHandle); 
      } 
     } 
    } 
} 

-Dan

1

En Delphi 2009, el código funciona mejor si se escribe de forma explícita s variables como saber AnsiString:

var s : Ansistring; 

dando el resultado esperado de C# después de la llamada:

outputInt = 2, outputString = "This is a test 2" 

en lugar de

outputInt = 2, outputString = "T" 
0

Es más fácil retirar una cadena usando la cadena PS:

function DelphiFunction(inputString : PAnsiChar; 
        var outputStringBuffer : PString; 
        var errorMsgBuffer : PString) 
        : WordBool; stdcall; export; 
var 
    s : string; 
begin 
    try 
    s := inputString; 
    outputStringBuffer:=PString(AnsiString(s)); 
    Result := true; 
    except 
    on e : exception do 
    begin 
     s:= 'error'; 
     errorMsgBuffer:=PString(AnsiString(e.Message)); 
     Result := false; 
    end; 
    end; 
end; 

en C# a continuación:

const int stringBufferSize = 1024; 

    var str = new IntPtr(stringBufferSize); 

    string loginResult = Marshal.PtrToStringAnsi(str); 
+1

Está devolviendo un puntero a una variable local en la pila. Una vez que 'DelphiFunction' regrese, esa variable no será válida. Todavía puede funcionar por suerte, pero no deberías confiar en ello. –

Cuestiones relacionadas