C# 调用 Delphi DLL 实例详解

追风的人
追风的人
发布时间:2026-10-15| 5人浏览过
原创

在使用 C# 开发项目时,偶尔需要集成遗留系统或高性能组件,其中可能包含 Delphi 编译的动态链接库(DLL)。由于两者在内存管理和数据类型定义上存在差异,直接调用可能导致崩溃或数据错误。本文将通过具体代码示例,演示如何在 C# 中正确声明 API、处理字符串返回值以及将非托管内存中的结构体安全转换为 C# 对象。

目标与准备

本示例旨在展示两种常见的跨平台交互场景:getResStr 返回一个以零结尾的字符指针,getMydata 返回一个包含整数、固定长度字符串、布尔值和浮点数的记录(Record)指针。为了完成此过程,您需要:

  1. Delphi DLL 源码:用于编译生成 .dll 文件。
  2. C# 项目:引用 System.Runtime.InteropServices 命名空间,以便使用 DllImport 和 Marshal 类。
  3. 依赖库位置:确保生成的 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# 互操作性处理的资料请关注本站其它相关文章!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。