如何在C#中使用带有PChar类型的Delphi Dll

3

以下是 Delphi DLL 代码:

library Project2;

uses
  SysUtils,
  Classes;

{$R *.res}

function SimpleConv(const s: string): string;
var
  i: Integer;
begin
  Result := '';
  for i := 1 to Length(s) do
    if Ord(S[i]) < 91 then
      Result := Result + S[i];
end;

function MsgEncode(pIn: pchar; InLen: Integer; var pOut: pchar; var OutLen: Integer): Boolean; stdcall;
var
  sIn: string;
  sOut: string;
begin
  SetLength(sIn, InLen);
  Move(pIn^, sIn[1], InLen);

  sOut := SimpleConv(sIn);   // Do something

  OutLen := Length(sOut);
  GetMem(pOut, OutLen);
  Move(sOut[1], pOut^, OutLen);
  Result := OutLen > 0;
end;

procedure BlockFree(Buf: pchar); stdcall;
begin
  if assigned(Buf) then
     FreeMem(Buf);
end;

exports
  MsgEncode,
  BlockFree;

begin
end.

Dll函数MsgEncode将分配内存到pOut参数,并使用BlockFree释放由MsgEncode分配的内存。

我的问题是:我如何在C#中使用这个dll?我是一个C#新手。


Delphi的哪个版本?(非常重要) - Warren P
3个回答

10

我会按照字面意思回答您的问题,但需要说明一些条件:

  • 了解您是否使用 Unicode Delphi 是很重要的,因为使用 PChar 进行交互的代码会在 AnsiChar 和 WideChar 之间浮动,具体取决于 Delphi 的版本。我假设您正在使用 Unicode Delphi。如果不是,则需要在 P/Invoke 的一侧更改字符串编组方式。
  • 我修改了您的 DLL 代码。我删除了长度参数,并且假定只有可信任的代码才能调用此 DLL。不可信的代码可能会导致缓冲区溢出,但您不会让不受信任的代码在计算机上运行,对吧?
  • 我还更改了 BlockFree ,使其可以接收未类型化的指针。它不需要被视为 PChar 类型,只会调用 Free

这是修改后的 Delphi 代码:

library Project2;

uses
  SysUtils;

{$R *.res}

function SimpleConv(const s: string): string;
begin
  Result := LowerCase(s);
end;

function MsgEncode(pIn: PWideChar; out pOut: PWideChar): LongBool; stdcall;
var
  sOut: string;
  BuffSize: Integer;
begin
  sOut := SimpleConv(pIn);
  BuffSize := SizeOf(Char)*(Length(sOut)+1);//+1 for null-terminator
  GetMem(pOut, BuffSize);
  FillChar(pOut^, BuffSize, 0);
  Result := Length(sOut)>0;
  if Result then
    Move(PChar(sOut)^, pOut^, BuffSize);
end;

procedure BlockFree(p: Pointer); stdcall;
begin
  FreeMem(p);//safe to call when p=nil
end;

exports
  MsgEncode,
  BlockFree;

begin
end.

这里是另一侧的C#代码:

using System;
using System.Runtime.InteropServices;

namespace ConsoleApplication1
{
    class Program
    {
        [DllImport("project2.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool MsgEncode(string pIn, out IntPtr pOut);

        [DllImport("project2.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
        public static extern void BlockFree(IntPtr p);

        static void Main(string[] args)
        {
            IntPtr pOut;
            string msg;
            if (MsgEncode("Hello from C#", out pOut))
                msg = Marshal.PtrToStringAuto(pOut);
                BlockFree(pOut);
        }
    }
}

这应该可以帮助你入门。由于你是C#的新手,需要花费一些时间阅读有关P/Invoke的资料。祝学习愉快!


2
@Warren,你不能这样做,而且这段代码也不会这样做!它在Delphi中使用相同的堆进行GetMem和FreeMem操作。 - David Heffernan
如果我使用Delphi 6来完成相同的功能,我该怎么办?我尝试了相同的步骤,但是"msg"的值将会是"桔獩椠⁳整瑳"。(我正在使用Delphi 6和Visual Studio 2010,.net framework 4.0) - bejarun
@bejarun 你知道PAnsiCharPWideChar之间的区别吗?以及AnsiStringUnicodeString之间的区别。另外还有PtrToStringAnsiPtrToStringUni之间的区别吗? - David Heffernan
请先阅读Marco Cantu的Unicode白皮书,不要在其他问题的评论中回答。 - David Heffernan
@Heffernan,PtrToStringAnsi 对我有用。现在文本正常显示了。谢谢。 - bejarun
显示剩余3条评论

4
注意,C#字符串数据是Unicode的,因此如果您继续使用PChar进行此Delphi代码并在PInvoke调用中执行从PChar到PWideChar的隐藏转换,则会发生另一个内存缓冲区的分配和所有数据复制到新缓冲区的操作。(转换意味着分配另一个内存缓冲区并将所有数据复制到新缓冲区) 如果您打算将此Delphi代码用于C#并且关心性能,则应该将Delphi代码更改为操作PWideChar数据。
使用PWideChar而不是PChar的另一个原因是,根据COM要求,Delphi使用Win32 SysAllocString分配器分配OleString类型。 这意味着字符串的接收者能够使用Win32 API释放它。
如果您实际上没有在函数中处理文本,但是使用PChar作为任意字节值数组的代理,那么您可以在调用的非托管端上这样做,但在托管端上却不能这样做。 如果是字节数据,则应将其声明为字节数组以避免字符集或字符大小转换。
在C#方面,您需要使用PInvoke来调用非托管的Delphi DLL函数。请参见pinvoke.net了解如何在C#中注释调用,以便PInvoke自动处理缓冲区分配的详细信息。查找一个传递PChar(或PWideChar)参数类似于您的函数的Win32 API函数,然后在PInvoke.net上搜索可在托管代码中使用的PInvoke声明。

@David:那么仍然使用通过SysAllocString分配的PWideChar是很重要的。 - Jeroen Wiert Pluimers
1
@David:宽Delphi:超出我的时间了。 - dthorpe
@David:是的,我试过了;感觉像是一个变通方法。这就是为什么我更喜欢PWideChar方法。所以如果它不是一个变通方法:请解释一下为什么它更好;-) - Jeroen Wiert Pluimers
@Jeroen 我不认为这是问题的更好解决方案。我只是认为OK可能没有P/Invoke的经验,所以改变接口的签名甚至比我已经做的更加混乱。我不是为OP做项目,只是提供指针。学习新概念很难 - 最好一次只学一个。今天我感觉不太受欢迎了吧?! 我最高票的答案只有两个可见字符(!!),而这个P/Invoke移植(我甚至不知道C#或任何.net)只得到了2个赞!! - David Heffernan
@Leo,我不知道你是否在关注,但dthorpe提到导出可以通过SysAllocString分配的BSTR将意味着您无需从DLL中导出解除分配器。这确实是正确的方法。 - David Heffernan
显示剩余3条评论

1

编辑过的

抱歉,我没有看到您也导出了BlockFree函数。

一个经验法则是:在同一个模块中分配和释放内存;如果您在Dll中分配了内存,则应该在同一Dll中释放。

因此,如果您使用BlockFree释放内存,则在同一个模块中分配和释放内存,这是可以的。

请注意,Delphi字符串和PChar类型是与版本相关的 - 在Delphi 2009之前为ANSI,在Delphi 2009及以后为UNICODE。


因为我需要在MsgEncode中实现复杂的计算,上面的代码只是一个简单的例子。 如果在主机应用程序中分配内存,需要分配多少?主机应用程序不知道。 是的,主机应用程序可以调用Msgencode两次,第一次计算内存大小,第二次实际分配内存。 但是,你知道MsgEncode非常复杂并且需要很长时间,从性能方面来说,我不希望调用它两次。 我希望在dll中分配和释放内存。 - Leo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接