C# 调用 Delphi DLL 实例详解
在使用 C# 开发项目时,偶尔需要集成遗留系统或高性能组件,其中可能包含 Delphi 编译的动态链接库(DLL)。由于两者在内存管理和数据类型定义上存在差异,直接调用可能导致崩溃或数据错误。本文将通过具体代码示例,演示如何在 C# 中正确声明 API、处理字符串返回值以及将非托管内存中的结构体安全转换为 C# 对象。
目标与准备
本示例旨在展示两种常见的跨平台交互场景:getResStr 返回一个以零结尾的字符指针,getMydata 返回一个包含整数、固定长度字符串、布尔值和浮点数的记录(Record)指针。为了完成此过程,您需要:
- Delphi DLL 源码:用于编译生成
.dll文件。 - C# 项目:引用
System.Runtime.InteropServices命名空间,以便使用DllImport和Marshal类。 - 依赖库位置:确保生成的
dllres.dll位于 C# 程序的可执行目录或系统 PATH 路径中。
Delphi DLL 源码分析
首先,我们需要明确 Delphi 端导出的函数签名和数据类型定义,这是 C# 端进行对应声明的基础。
library dllres;
type
char10 = array[0..9] of char;
TMydata = packed record
id: Integer;
name: char10;
married: Boolean;
salary: Double;
end;
PMydata = ^TMydata;
const
RESSTR: array[0..4] of string = ('HELLO', 'COLOR', 'DELPHI', 'shared', 'library');
NO_RESULT = 'no result';
var
mydata: TMydata;
{$R *.res}
// 返回字符串指针
function getResStr(aindex: Integer): PChar; stdcall;
begin
if aindex < Length(RESSTR) then
begin
Result := pchar(RESSTR[aindex]);
end
else
begin
Result := pchar(NO_RESULT);
end;
end;
// 返回结构体指针
function getMydata: PMydata; stdcall;
begin
with mydata do
begin
id := 123;
name := 'obama';
married := false;
salary := 1200;
end;
Result := @mydata;
end;
exports getResStr, getMydata;
begin
end.
关键点说明:
- packed record:表示紧凑排列,无填充字节,这对计算 C# 结构体大小时至关重要。
- stdcall:调用约定必须匹配,否则会导致栈不平衡。
- PChar/PMydata:这些是指针类型,在 C# 中通常映射为
IntPtr。
C# 调用步骤
第1步:声明外部方法 (P/Invoke)
使用 [DllImport] 特性指定 DLL 名称及调用约定,将函数声明为静态外部方法。返回值类型使用 IntPtr,因为它们是原生指针。
using System;
using System.Runtime.InteropServices;
using System.Text;
class Invoke_Delphi_Dll_Exam
{
// 注意:此处根据实际需求调整 CallingConvention
[DllImport("dllres.dll", CallingConvention = CallingConvention.StdCall)]
public static extern IntPtr getResStr(int index);
[DllImport("dllres.dll", CallingConvention = CallingConvention.StdCall)]
public static extern IntPtr getMydata();
public struct Mydata
{
// ... 后续定义
}
// ... 其余代码
}
第2步:定义对应的结构体映射
Delphi 中的 TMydata 布局如下:id (4字节), name (10字节字符数组, 但实际存储可能涉及宽字符或ANSI取决于编译设置,此处假设ANSI单字节存于偏移后,需注意内存大小计算), married (1字节/2字节视平台而定), salary (8字节Double)。
原示例中采用了一种较为底层的字节数组解析方式,总长度为33字节。这种方式的优点是无需精确控制字段偏移量带来的 padding 问题,而是直接从原始内存块读取。
public struct Mydata
{
public int id; // 占据4字节
public string name; // 变长引用
public bool married; // 占据1-2字节
public double salary;// 占据8字节
// 构造函数:从非托管内存拷贝的字节数组中解析
public Mydata(byte[] data)
{
if (data != null && data.Length == 33)
{
// 从索引0开始读取4字节的Integer
id = BitConverter.ToInt32(data, 0);
// 从索引4开始读取20字节作为Unicode字符串(Delphi String有时表现为宽字符或在某些环境下占用双倍空间,此处依原文逻辑为20字节)
// 注意:替换空字符 \0
name = Encoding.Unicode.GetString(data, 4, 20).Replace("\0", "");
// 从索引24开始读取Boolean
married = BitConverter.ToBoolean(data, 24);
// 从索引25开始读取Double (8字节)
salary = BitConverter.ToDouble(data, 25);
}
else
{
// 默认值初始化
id = 0;
name = String.Empty;
married = false;
salary = 0;
}
}
public override string ToString()
{
return String.Format("id: {0}, name: {1}, married: {2}, salary: {3}",
id, name, married, salary);
}
}
第3步:执行调用并释放资源
在 Main 方法中,我们演示如何获取字符串和处理结构体内存。
private static void Main(string[] args)
{
// 1. 调用返回字符串的函数
// Marshal.PtrToStringAuto 自动根据系统区域设置解码 ANSI 或 Unicode
Console.WriteLine(Marshal.PtrToStringAuto(getResStr(0)));
// 2. 调用返回结构体指针的函数
IntPtr ptr = getMydata();
// 预分配目标缓冲区,大小必须与源数据结构一致
byte[] data = new byte[33];
// 将非托管内存内容复制到托管数组
Marshal.Copy(ptr, data, 0, 33);
// 3. 解析数据
Mydata mydataObj = new Mydata(data);
Console.WriteLine(mydataObj);
}
注意事项与排错
- 内存泄漏风险:上述
getMydata的实现中,Delphi 返回的是全局变量mydata的地址。如果在 C# 侧多次调用且期望获得独立副本,需小心重复使用同一地址可能导致的数据覆盖问题。在生产环境中,建议 DLL 提供类似AllocMem并让调用方负责释放内存,或使用复制语义而非指针语义。 - 字节序与对齐:虽然示例使用了手动
byte[]解析规避了 StructLayout 的对齐陷阱,但在更复杂的结构中,建议使用[StructLayout(LayoutKind.Sequential, Pack=1)]来强制打包,或通过Marshal.SizeOf验证大小是否一致。 - 字符串编码:
Encoding.Unicode.GetString假设数据是 UTF-16 LE。如果 Delphi 使用的是 ANSI (AnsiString),应改用Encoding.Default或Encoding.Ansi。请根据实际的 Delphi 编译器版本和字符串类型确认编码格式。
总结
通过 DllImport 结合 Marshal 类,C# 可以灵活地与 Delphi DLL 进行交互。核心在于准确理解非托管内存布局,并通过字节级操作或结构映射来转换数据类型。以上就是 C# 调用 Delphi DLL 的详细内容,更多关于 C# 互操作性处理的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













