編集:これのより良い実装を以下に投稿しました。これをここに残して、応答が意味を持つようにします。
DelphiでDLLを記述し、C#から呼び出して文字列を渡して返すことができるようにするための正しい方法を数多く検索しました。多くの情報が不完全または不正確でした。多くの試行錯誤の末、私は解決策を見つけました。
これは、Delphi 2007とVS 2010を使用してコンパイルされました。他のバージョンでも正常に動作すると思われます。
これがDelphiコードです。プロジェクトにバージョン情報を含めることを忘れないでください。
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.
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();
}
}
}
この情報が他の誰かが私ほど髪を抜かなくても済むようになることを願っています。
私の投稿への応答に基づいて、単にPAnsiCharsを返すのではなく、返された文字列に文字列バッファを使用する新しい例を作成しました。
Delphi DLLソース:
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#コード:
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();
}
}
}
そして、DLLを動的にロードする方法を示す追加のクラスがあります(長い行で申し訳ありません):
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);
}
}
}
}
-ダン
Jeroen Pluimersがコメントで述べたように、Delphi文字列は参照カウントされることに注意してください。
IMO、異機種環境で文字列を返すことが想定される状況では、結果のバッファーを提供するように呼び出し元に要求し、関数はそのバッファーを埋める必要があります。このようにして、呼び出し元は、バッファを作成し、それが使用されたときに破棄する必要があります。 Win32 API関数を見ると、呼び出し元に文字列を返す必要があるときに同じように機能することがわかります。
そのためには、関数パラメーターのタイプとしてPChar(PAnsiCharまたはPWideCharのいずれか)を使用できますが、呼び出し元にもバッファーのサイズを提供するように依頼する必要があります。サンプルソースコードについては、以下のリンクで私の回答をご覧ください。
Freepascalコンパイル済みの文字列(PChar)の交換DLLとDelphiコンパイル済みEXE
質問は特にFreePascalとDelphiの間で文字列を交換することに関するものですが、アイデアと回答はあなたのケースにも当てはまります。
Delphi 2009では、変数sを明示的にAnsiStringとして入力すると、コードの動作が向上します。
var s : Ansistring;
呼び出し後にC#から期待される結果を与える:
outputInt = 2, outputString = "This is a test 2"
の代わりに
outputInt = 2, outputString = "T"
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;
C#では:
const int stringBufferSize = 1024;
var str = new IntPtr(stringBufferSize);
string loginResult = Marshal.PtrToStringAnsi(str);