EDIT: J'ai posté une meilleure mise en œuvre de cela, ci-dessous. J'ai laissé ceci ici pour que les réponses aient un sens.
J'ai effectué de nombreuses recherches pour trouver la bonne méthode pour écrire DLL dans Delphi et pouvoir l'appeler à partir de C #, en passant et en renvoyant des chaînes. Une grande partie de l'information était incomplète ou incorrecte. Après beaucoup d'essais et d'erreurs, j'ai trouvé la solution.
Ceci a été compilé avec Delphi 2007 et VS 2010. Je suppose que cela fonctionnera également dans d'autres versions.
Voici le code Delphi. N'oubliez pas d'inclure les informations de version dans le projet.
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.
Voici le code 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();
}
}
}
J'espère que cette information aidera quelqu'un d'autre à ne pas avoir à se couper les cheveux autant que moi.
Sur la base des réponses à mon message, j'ai créé un nouvel exemple qui utilise des tampons de chaîne pour les chaînes renvoyées, au lieu de simplement renvoyer PAnsiChars.
Delphi DLL source:
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.
Code 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 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();
}
}
}
Et voici une classe supplémentaire qui montre comment charger le DLL de manière dynamique (désolé pour les longues lignes):
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
Comme Jeroen Pluimers l’a dit dans son commentaire, vous devez noter que les chaînes Delphi sont comptées en nombre de références.
OMI, dans les cas où vous êtes censé renvoyer une chaîne dans des environnements hétérogènes, vous devez demander à l'appelant de fournir un tampon pour le résultat et la fonction doit remplir ce tampon. De cette façon, l'appelant est responsable de la création du tampon et de son élimination lorsque c'est terminé. Si vous examinez les fonctions de l'API Win32, vous verrez qu'elles font la même chose lorsqu'elles doivent renvoyer une chaîne à un appelant.
Pour ce faire, vous pouvez utiliser PChar (PAnsiChar ou PWideChar) comme type de paramètre de fonction, mais vous devez également demander à l'appelant de fournir également la taille de la mémoire tampon. Regardez ma réponse dans le lien ci-dessous, pour un exemple de code source:
Échange de chaînes (PChar) entre un fichier DLL et un fichier EXE compilé Delphi
La question concerne spécifiquement l'échange de chaînes entre FreePascal et Delphi, mais l'idée et la réponse sont également applicables à votre cas.
Dans Delphi 2009, le code fonctionne mieux si vous tapez explicitement la variable s en tant qu'AnsiString:
var s : Ansistring;
donnant le résultat attendu de C # après l'appel:
outputInt = 2, outputString = "This is a test 2"
au lieu de
outputInt = 2, outputString = "T"
Il est plus facile de récupérer une chaîne en utilisant String:
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 # alors:
const int stringBufferSize = 1024;
var str = new IntPtr(stringBufferSize);
string loginResult = Marshal.PtrToStringAnsi(str);