如何通过基类复用LibraryImport实现多DLL相同函数导入?
我正在创建一个.NET WebAPI,用以逐步将Delphi代码迁移至C#,但现有代码库规模超过150万行。因此我们计划将应用拆分为可加载到C# API中的小型库,并逐步重写其中的函数或服务。我们规定所有创建的DLL都要导出Initialize、Execute、DisposeStr这三个通用函数,以统一与原生代码交互的方式。
问题在于,我需要创建DelphiEmployeeService.cs、DelphiWorktimeService.cs等上百个服务包装类,每个类开头都要重复编写几乎相同的导入代码:
[LibraryImport("<DLLNAME>", EntryPoint = "Initialize")] [return: MarshalAs(UnmanagedType.Bool)] [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvStdcall) })] private static partial bool Initialize(); [LibraryImport("<DLLNAME>", EntryPoint = "Execute", StringMarshalling = StringMarshalling.Utf16)] [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvStdcall) })] private static partial IntPtr Execute(string parameters); [LibraryImport("<DLLNAME>", EntryPoint = "DisposeStr")] [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvStdcall) })] private static partial void DisposeStr(IntPtr str);
为了避免重复代码,我想创建一个DelphiImplementation.cs基类,但不确定如何让子类指定要加载的DLL名称。我期望的实现方式如下:
public class DelphiImplementation { protected abstract string LibraryName; [LibraryImport(LibraryName, EntryPoint = "Initialize")] [return: MarshalAs(UnmanagedType.Bool)] [UnmanagedCallConv(CallConvs = new Type[] {typeof(System.Runtime.CompilerServices.CallConvStdcall) })] private static partial bool Initialize(); [LibraryImport(LibraryName, EntryPoint = "Execute", StringMarshalling = StringMarshalling.Utf16)] [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvStdcall) })] private static partial IntPtr Execute(string parameters); [LibraryImport(LibraryName, EntryPoint = "DisposeStr")] [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvStdcall) })] private static partial void DisposeStr(IntPtr str); } public class EmployeeDelphiService : DelphiImplementation { protected override string LibraryName => "Employee.dll"; }
请问这样的实现是否可行?
这种实现不可行,核心原因是[LibraryImport]特性的第一个参数(库名称)必须是编译时常量,而抽象属性LibraryName属于运行时才能确定的值,编译器无法在编译阶段解析它,会直接抛出编译错误。
针对你的场景,有几种更合适的替代方案:
方案1:使用泛型基类+源代码生成器(推荐)
利用.NET的源代码生成器,根据子类指定的库名称自动生成LibraryImport方法。你可以定义一个基类标记,然后编写生成器扫描所有继承该标记的类,自动生成对应的导入代码。
示例标记基类:
public abstract class DelphiServiceBase { protected abstract string LibraryName { get; } // 定义抽象方法让生成器实现 public abstract bool Initialize(); public abstract IntPtr Execute(string parameters); public abstract void DisposeStr(IntPtr str); }
生成器会在编译时为每个子类(比如EmployeeDelphiService)生成包含LibraryImport的具体实现,彻底避免手动重复代码。
方案2:手动使用Marshal.GetDelegateForFunctionPointer绑定
放弃LibraryImport,改为手动加载DLL并绑定函数委托,这样可以在运行时指定库名称。
基类实现示例:
public abstract class DelphiImplementation : IDisposable { protected abstract string LibraryName { get; } private IntPtr _libraryHandle; // 定义委托类型 private delegate bool InitializeDelegate(); private delegate IntPtr ExecuteDelegate([MarshalAs(UnmanagedType.LPWStr)] string parameters); private delegate void DisposeStrDelegate(IntPtr str); private InitializeDelegate? _initialize; private ExecuteDelegate? _execute; private DisposeStrDelegate? _disposeStr; protected DelphiImplementation() { _libraryHandle = NativeLibrary.Load(LibraryName); if (_libraryHandle == IntPtr.Zero) throw new DllNotFoundException($"Failed to load {LibraryName}"); // 绑定函数 _initialize = Marshal.GetDelegateForFunctionPointer<InitializeDelegate>( NativeLibrary.GetExport(_libraryHandle, "Initialize")); _execute = Marshal.GetDelegateForFunctionPointer<ExecuteDelegate>( NativeLibrary.GetExport(_libraryHandle, "Execute")); _disposeStr = Marshal.GetDelegateForFunctionPointer<DisposeStrDelegate>( NativeLibrary.GetExport(_libraryHandle, "DisposeStr")); } public bool Initialize() => _initialize!(); public IntPtr Execute(string parameters) => _execute!(parameters); public void DisposeStr(IntPtr str) => _disposeStr!(str); public void Dispose() { if (_libraryHandle != IntPtr.Zero) { NativeLibrary.Free(_libraryHandle); _libraryHandle = IntPtr.Zero; } } } // 子类实现 public class EmployeeDelphiService : DelphiImplementation { protected override string LibraryName => "Employee.dll"; }
这个方案的优点是完全支持运行时指定库名称,缺点是需要手动处理委托绑定和DLL的加载/释放,性能略低于LibraryImport(但大多数业务场景下差异可忽略)。
方案3:使用T4模板生成重复代码
如果不想用源代码生成器,也可以用T4模板批量生成所有服务类的代码。你可以在模板中枚举所有需要的DLL名称,然后自动生成每个服务类的完整代码,包括LibraryImport部分。
这种方式简单直接,适合一次性生成大量相似代码,但灵活性不如源代码生成器(后续新增服务需要修改模板重新生成)。
内容的提问来源于stack exchange,提问作者Matt Baech

