简介:本资源是一份面向C#初学者与.NET开发者的DLL封装与调用实战教学包,聚焦代码复用、模块解耦与跨项目集成等核心工程实践。内容覆盖从类库创建、接口设计、异常处理到引用调用、延迟加载及COM互操作的完整链路,特别适合正在学习组件化开发或需对接遗留系统的开发者。压缩包共38个文件,含8个核心C#源码(.cs)、3个可执行示例(.exe)、3个生成的DLL及配套项目文件(.csproj/.sln)、调试符号(.pdb)和资源文件(.resx/.resources),结构清晰,便于逐层理解编译、引用与运行机制;整体仅53KB,轻量易解压即学。目前已有3570人学习下载,资源附带完整VS解决方案目录(DllTest与TestDll双项目),包含窗体界面、控制台调用、类库定义及多层bin/obj输出结构,是掌握.NET动态库开发流程的典型入门范例。
1. C# DLL 封装与调用:不是“打个包就能用”,而是跨进程边界的契约设计
你写好了一个功能完整的 C# 类库,MathHelper.cs里封装了高精度数值积分、矩阵奇异值分解和自适应插值——但把它编译成.dll后,隔壁组的 C++ 工程师说“加载失败”,Python 同事说“找不到入口”,甚至你自己在另一个 .NET 项目里Assembly.LoadFrom之后调Type.GetMethod却返回 null。这不是代码有 bug,而是你漏掉了 DLL 封装中最关键的一环:它从来不是“把代码塞进文件”,而是在 CLR、Windows PE 加载器、P/Invoke 调用约定三重边界上,亲手签一份可执行的契约。这份资源不是教你怎么点“生成解决方案”,而是带你从 IL 元数据签名开始,逐层拆解public static extern怎么对齐__stdcall,[UnmanagedFunctionPointer]如何决定回调函数能否被 C++ 安全捕获,以及为什么DllImport("MyLib.dll")在 x64 进程里会静默失败——哪怕你的 DLL 确实存在。适合正在做混合编程、嵌入式上位机对接、或需要将 .NET 算法模块交付给非 .NET 系统的开发者。别再让 DLL 成为黑匣子;这次,我们把它焊死在 ABI 接口上。
2. 封装侧:C# DLL 的三层构建逻辑——从托管类库到导出函数的显式暴露
DLL 封装不是“右键 → 生成”就完事。C# 默认生成的是托管 DLL(.dll文件本质是 .NET 程序集),它依赖 CLR 运行时,无法被 C/C++ 直接LoadLibrary+GetProcAddress调用。要真正实现跨语言互操作,必须明确选择封装路径:纯托管封装供 .NET 消费者使用,或显式导出非托管函数供原生代码调用。本节聚焦后者——这是工程落地中最易踩坑、也最常被文档忽略的硬核路径。
2.1 托管类库封装:基础但必须严谨的 .NET 内部复用
这是最常见场景:你有一套业务逻辑,想在多个 .NET 项目中复用。此时封装目标是生成一个强命名、版本可控、无隐式依赖的程序集。
dotnet new classlib -n MathCoreLib cd MathCoreLib关键不在代码,而在.csproj配置。默认生成的 SDK 风格项目已足够干净,但必须显式控制以下三点:
- 目标框架:若需兼容旧版 .NET Framework(如 4.7.2),需改
<TargetFramework>net472</TargetFramework>;若仅用于 .NET 6+,保留net6.0即可。混用会导致运行时找不到System.Runtime。 - 程序集版本与强名称:在
<PropertyGroup>中添加:<AssemblyVersion>1.2.0.0</AssemblyVersion> <FileVersion>1.2.0.0</FileVersion> <GenerateAssemblyInfo>true</GenerateAssemblyInfo> <SignAssembly>true</SignAssembly> <AssemblyOriginatorKeyFile>key.snk</AssemblyOriginatorKeyFile>key.snk通过sn -k key.snk生成。强名称确保 GAC 注册和版本策略生效,避免“相同名字不同实现”的 DLL Hell。 - 公开 API 控制:默认所有
public类型都导出。若内部有public工具类不希望被外部引用,加[EditorBrowsable(EditorBrowsableState.Never)]或直接设为internal,并在AssemblyInfo.cs中添加:[assembly: InternalsVisibleTo("UnitTestProject")] // 仅允许测试项目访问 internal
编译后得到MathCoreLib.dll,它是一个标准 .NET 程序集。其他 .NET 项目可通过 NuGet 包引用或直接Add Project Reference消费。注意:此 DLL 无法被 C++LoadLibrary加载——Windows 加载器会报ERROR_BAD_EXE_FORMAT,因为它是 PE32+ 带 CLR 头的托管映像,不是原生 DLL。
2.2 非托管函数导出:用 C++/CLI 或 UnmanagedExports 实现真正的DllExport
这才是让 C/C++、Python、Delphi 等原生环境能dlopen/LoadLibrary的核心。C# 原生不支持__declspec(dllexport),必须借助桥接技术。主流方案有两种:C++/CLI 混合模式封装(微软官方推荐,但学习成本高),和Robert Giesecke 的 UnmanagedExports 工具(轻量、成熟、社区验证)。
我们采用 UnmanagedExports,因其零配置、零依赖、且生成的导出表完全符合 Windows PE 规范。步骤如下:
- 在
MathCoreLib项目中安装 NuGet 包:dotnet add package UnmanagedExports - 编写导出函数。必须满足三个硬性条件:
- 方法必须是
static - 必须标记
[DllExport]特性(来自RGiesecke.DllExport命名空间) - 参数与返回值类型必须是 C 兼容基本类型(
int,double,IntPtr,string需特殊处理)
- 方法必须是
using System; using System.Runtime.InteropServices; using RGiesecke.DllExport; public static class NativeApi { // ✅ 正确:返回 int,参数为 double,符合 C ABI [DllExport("CalculateSqrt", CallingConvention = CallingConvention.StdCall)] public static int CalculateSqrt(double input, out double result) { try { result = Math.Sqrt(input); return 0; // success } catch (Exception) { result = 0.0; return -1; // error code } } // ✅ 正确:字符串传入需用 IntPtr + Marshal.StringToHGlobalAnsi [DllExport("ProcessText", CallingConvention = CallingConvention.StdCall)] public static int ProcessText(IntPtr inputStr, IntPtr outputBuf, int bufSize) { if (inputStr == IntPtr.Zero || outputBuf == IntPtr.Zero) return -1; string input = Marshal.PtrToStringAnsi(inputStr); string processed = input.ToUpperInvariant() + "_PROCESSED"; // 确保不越界:复制 min(len+1, bufSize) 字节 byte[] bytes = System.Text.Encoding.ASCII.GetBytes(processed + "\0"); int copyLen = Math.Min(bytes.Length, bufSize); Marshal.Copy(bytes, 0, outputBuf, copyLen); return copyLen; } }- 关键:修改
.csproj,启用 UnmanagedExports 构建任务:<PropertyGroup> <TargetFramework>net6.0-windows</TargetFramework> <!-- 必须指定 -windows --> <Platforms>x64</Platforms> <!-- 导出函数平台必须明确 --> </PropertyGroup> <Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Exec Command=""$(MSBuildThisFileDirectory)tools\unmanagedexports\RGiesecke.DllExport.MSBuild.Task.dll" "$(TargetPath)"" /> </Target>
编译后,用dumpbin /exports MathCoreLib.dll查看导出表,你会看到:
ordinal hint RVA name 1 0 00001230 CalculateSqrt 2 1 000012A0 ProcessText这证明函数已真实写入 PE 文件的 Export Directory,C++ 可以GetProcAddress(hMod, "CalculateSqrt")安全调用。
提示:
CallingConvention.StdCall是 Windows API 默认调用约定,C++ 侧必须匹配。若用Cdecl,C++ 调用时需手动清理栈,极易崩溃。
2.3 导出函数的内存契约:谁分配、谁释放?
这是跨语言调用中最隐蔽的雷区。看ProcessText示例:C++ 传入outputBuf是由 C++ 分配的缓冲区,C# 仅负责写入。绝对禁止在 C# 中用Marshal.AllocHGlobal分配内存并返回指针给 C++ 释放——因为Marshal.FreeHGlobal和 C++ 的free()可能使用不同堆管理器,导致Access Violation。
正确做法只有两种:
- C++ 分配,C# 填充(如上例):C++ 负责
malloc/new和free/delete,C# 只Marshal.Copy。 - C# 分配,C# 提供释放函数:导出第二个函数
FreeStringBuffer(IntPtr ptr),内部调用Marshal.FreeHGlobal(ptr),C++ 调用此函数释放。
[DllExport("FreeStringBuffer", CallingConvention = CallingConvention.StdCall)] public static void FreeStringBuffer(IntPtr ptr) { if (ptr != IntPtr.Zero) Marshal.FreeHGlobal(ptr); }违反此契约的后果不是立即崩溃,而是堆损坏、随机 crash,调试难度极高。血泪经验:所有跨 DLL 边界的内存操作,必须在接口文档里白纸黑字写明归属方。
3. 调用侧:C++、Python、C# 自身如何安全加载与调用
封装完成只是第一步。调用方若不了解加载时机、符号解析规则、异常穿透机制,同样会翻车。本节覆盖三大主流调用场景,每一步都附可验证命令与错误日志特征。
3.1 C++ 原生调用:显式加载与函数指针绑定
C++ 调用 C# 导出 DLL,必须走 Win32 API 显式加载流程。隐式链接(.lib+#pragma comment(lib, "..."))完全无效,因为 C# DLL 不生成导入库。
// MathCaller.cpp #include <windows.h> #include <iostream> #include <string> // 1. 定义函数指针类型,必须与 C# 导出签名严格一致 typedef int (__stdcall *CalculateSqrtFunc)(double, double*); typedef int (__stdcall *ProcessTextFunc)(LPVOID, LPVOID, int); typedef void (__stdcall *FreeStringBufferFunc)(LPVOID); int main() { // 2. 显式加载 DLL —— 路径必须绝对或在 PATH 中 HMODULE hDll = LoadLibrary(L"MathCoreLib.dll"); if (!hDll) { DWORD err = GetLastError(); std::wcout << L"LoadLibrary failed: " << err << std::endl; return -1; } // 3. 获取函数地址 —— 名称必须与 [DllExport] 中字符串完全匹配(区分大小写!) CalculateSqrtFunc pCalc = (CalculateSqrtFunc)GetProcAddress(hDll, "CalculateSqrt"); if (!pCalc) { std::wcout << L"GetProcAddress(CalculateSqrt) failed" << std::endl; FreeLibrary(hDll); return -1; } // 4. 调用函数 double result; int ret = pCalc(16.0, &result); if (ret == 0) { std::wcout << L"sqrt(16) = " << result << std::endl; } // 5. 字符串处理:C++ 分配缓冲区 const char* input = "hello"; char outputBuf[256] = {0}; ProcessTextFunc pProc = (ProcessTextFunc)GetProcAddress(hDll, "ProcessText"); if (pProc) { int len = pProc((LPVOID)input, (LPVOID)outputBuf, sizeof(outputBuf)); std::cout << "Processed: " << outputBuf << std::endl; } FreeLibrary(hDll); return 0; }编译命令(x64):
cl /EHsc /W4 MathCaller.cpp user32.lib注意:
LoadLibrary失败常见原因:DLL 依赖的 .NET Runtime 未安装(需 .NET 6 Desktop Runtime)、DLL 位数与主程序不匹配(x64 程序不能加载 x86 DLL)、路径错误(建议用绝对路径测试)。用Process Monitor工具过滤MathCoreLib.dll可精准定位加载失败环节。
3.2 Python 调用:ctypes 的类型映射与错误处理
Python 通过ctypes调用,比 C++ 更脆弱,因类型自动转换易出错。必须显式声明argtypes和restype。
# call_math.py import ctypes import sys from pathlib import Path # 1. 加载 DLL —— 使用绝对路径避免 cwd 问题 dll_path = Path(__file__).parent / "MathCoreLib.dll" if not dll_path.exists(): raise FileNotFoundError(f"DLL not found: {dll_path}") dll = ctypes.CDLL(str(dll_path)) # 2. 绑定函数并声明签名 # CalculateSqrt(double, *double) -> int dll.CalculateSqrt.argtypes = [ctypes.c_double, ctypes.POINTER(ctypes.c_double)] dll.CalculateSqrt.restype = ctypes.c_int # ProcessText(char*, char*, int) -> int dll.ProcessText.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_int] dll.ProcessText.restype = ctypes.c_int # 3. 调用 result = ctypes.c_double(0.0) ret = dll.CalculateSqrt(25.0, ctypes.byref(result)) if ret == 0: print(f"sqrt(25) = {result.value}") # 字符串处理:Python 字节串传入,C# 写入 output_buf input_bytes = b"world" output_buf = ctypes.create_string_buffer(256) ret = dll.ProcessText(input_bytes, output_buf, len(output_buf)) if ret > 0: print(f"Processed: {output_buf.value.decode('ascii')}") # 4. 错误检查:ctypes 本身不抛异常,需手动检查 if not hasattr(dll, 'CalculateSqrt'): raise AttributeError("Function 'CalculateSqrt' not found in DLL")运行前确保:
- Python 为 x64 版本(与 DLL 位数一致)
- 已安装 .NET 6+ Desktop Runtime(
dotnet --list-runtimes验证) - 若报
OSError: [WinError 126] 找不到指定的模块,用Dependencies.exe(替代旧版 Dependency Walker)打开 DLL,查看缺失的VCRUNTIME140.dll或api-ms-win-crt-*.dll—— 安装 Visual C++ Redistributable for Visual Studio 2015-2022 即可。
3.3 C# 自身调用:反射加载与动态调用的边界
同一个 C# 项目调用自己生成的 DLL?看似多余,实则用于插件化架构或热更新。此时不能用using引用,必须Assembly.LoadFrom+Activator.CreateInstance。
// PluginLoader.cs using System; using System.IO; using System.Reflection; public class PluginLoader { public static void LoadAndInvoke() { string dllPath = Path.Combine(AppContext.BaseDirectory, "MathCoreLib.dll"); if (!File.Exists(dllPath)) throw new FileNotFoundException($"Plugin DLL not found: {dllPath}"); // 1. 加载程序集(非托管 DLL 会失败!必须是托管 DLL) Assembly asm = Assembly.LoadFrom(dllPath); // 2. 获取类型(需知道完整命名空间+类名) Type helperType = asm.GetType("MathCoreLib.MathHelper"); if (helperType == null) throw new InvalidOperationException("Type MathCoreLib.MathHelper not found"); // 3. 创建实例并调用方法 object instance = Activator.CreateInstance(helperType); MethodInfo method = helperType.GetMethod("IntegrateFunction"); if (method == null) throw new InvalidOperationException("Method IntegrateFunction not found"); // 4. 准备参数:此处假设方法签名为 double IntegrateFunction(Func<double,double> f, double a, double b) var f = new Func<double, double>(x => x * x); // x^2 object[] args = { f, 0.0, 1.0 }; double result = (double)method.Invoke(instance, args); Console.WriteLine($"Integral of x^2 from 0 to 1 = {result}"); } }注意:此方式仅适用于托管类库 DLL。若你导出的是非托管函数(
[DllExport]),Assembly.LoadFrom无法解析其导出表,必须改用DllImport声明:[DllImport("MathCoreLib.dll", CallingConvention = CallingConvention.StdCall)] public static extern int CalculateSqrt(double input, out double result);
4. 避坑:五个高频翻车现场与血泪排查指南
封装和调用过程中的错误,90% 都有固定模式。以下是我在某图像处理 Demo 项目中踩过的真坑,按现象→原因→解决结构整理,每一条都对应过真实崩溃日志。
4.1 现象:C++LoadLibrary返回NULL,GetLastError()= 126(模块未找到)
- 原因:DLL 依赖的 .NET Runtime 未安装,或位数不匹配(x64 程序尝试加载 x86 DLL),或 DLL 本身是托管程序集(未用 UnmanagedExports)。
- 排查:
- 用
corflags MathCoreLib.dll检查是否为 IL-only(输出含ILONLY则是托管 DLL,不能被LoadLibrary直接加载); - 用
dumpbin /headers MathCoreLib.dll | findstr "machine"确认机器类型(8664 machine (x64)表示 x64); - 运行
dotnet --list-runtimes,确认目标机器已安装对应版本(如Microsoft.NETCore.App 6.0.0)。
- 用
- 解决:安装对应 .NET Desktop Runtime;确保编译平台与主程序一致;若需原生调用,必须用 UnmanagedExports 生成非托管导出。
4.2 现象:C++GetProcAddress返回NULL,但LoadLibrary成功
- 原因:导出函数名拼写错误(大小写敏感)、C# 项目未启用 UnmanagedExports 构建任务、或
CallingConvention不匹配导致符号名修饰(mangling)。 - 排查:
- 用
dumpbin /exports MathCoreLib.dll确认导出函数名是否为原始字符串(如CalculateSqrt),而非修饰名(如_CalculateSqrt@8); - 检查
.csproj中是否包含<PackageReference Include="UnmanagedExports" />且构建事件已执行; - 在 C# 函数上加
CallingConvention = CallingConvention.StdCall,避免Cdecl导致的@N后缀。
- 用
- 解决:统一使用
StdCall;用dumpbin验证导出名;C++ 调用时字符串严格匹配。
4.3 现象:Pythonctypes.CDLL成功,但调用函数时OSError: exception: access violation reading 0x0000000000000000
- 原因:
argtypes声明错误,导致 ctypes 传递了错误的指针类型(如该传ctypes.c_char_p却传了str),或 C# 函数内解引用了空指针。 - 排查:
- 在 C# 函数开头加
if (inputStr == IntPtr.Zero) throw new ArgumentNullException(nameof(inputStr));并捕获异常; - 用
ctypes.windll.kernel32.GetLastError()在 Python 中获取 Windows 错误码; - 用
print(ctypes.sizeof(ctypes.c_char_p))确认指针大小(x64 下应为 8)。
- 在 C# 函数开头加
- 解决:Python 中所有指针参数必须用
ctypes.c_char_p、ctypes.c_void_p等显式类型;字符串传入前用.encode('utf-8');缓冲区用ctypes.create_string_buffer(N)。
4.4 现象:C# 反射调用GetMethod返回null,但类名正确
- 原因:方法是
private或internal,或未在AssemblyInfo.cs中声明InternalsVisibleTo,或方法签名(参数类型、泛型)与反射查询不完全匹配。 - 排查:
- 用
ildasm MathCoreLib.dll打开程序集,展开类节点,确认方法是否在列表中且为public; - 用
helperType.GetMethods(BindingFlags.Public | BindingFlags.Instance)列出所有公有实例方法,确认是否存在; - 检查参数类型:
typeof(double)vstypeof(Double)是同一类型,但typeof(Func<,>)需用MakeGenericType构造。
- 用
- 解决:方法必须
public;若需调用internal方法,添加[assembly: InternalsVisibleTo("YourCallerAssembly")];反射时用GetMethods().FirstOrDefault(m => m.Name == "xxx")替代GetMethod("xxx")避免重载歧义。
4.5 现象:DLL 在开发机运行正常,部署到客户机后System.IO.FileNotFoundException报告找不到System.Runtime
- 原因:客户机未安装 .NET Desktop Runtime,或安装了 Server Core 版本(不含桌面组件)。
- 排查:
- 客户机运行
dotnet --list-runtimes,确认Microsoft.WindowsDesktop.App是否存在(非Microsoft.NETCore.App); - 检查事件查看器 → Windows 日志 → 应用程序,筛选来源为
.NET Runtime的错误事件; - 用
procmon.exe监控进程,过滤Result为NAME NOT FOUND的CreateFile操作。
- 客户机运行
- 解决:部署时一并安装
.NET 6 Desktop Runtime(离线安装包约 120MB);或改用self-contained发布(dotnet publish -r win-x64 --self-contained true),但体积增大至 150MB+。
5. 进阶技巧:用 PowerShell 自动化验证与跨平台兼容性兜底
当你的 DLL 要交付给多个团队、多种环境时,手动验证每个调用场景效率极低。我给自己写的 PowerShell 脚本,能在 30 秒内完成五项关键检查:位数校验、导出函数扫描、.NET 依赖分析、C++ 加载测试、Python ctypes 调用验证。它不是玩具,而是上线前的“后悔药”。
5.1 PowerShell 自动化验证脚本
# Validate-Dll.ps1 param( [Parameter(Mandatory)] [string]$DllPath, [ValidateSet("x64", "x86")] [string]$ExpectedArch = "x64" ) Write-Host "🔍 Validating $DllPath..." -ForegroundColor Green # 1. 检查文件存在与权限 if (-not (Test-Path $DllPath)) { throw "DLL not found: $DllPath" } if (-not (Get-Item $DllPath).IsReadOnly) { Write-Warning "DLL is writable — consider setting read-only for production" } # 2. 检查 PE 架构(使用 Get-PEHeader,需提前安装 PowerShell module) try { $pe = Get-PEHeader $DllPath $arch = switch ($pe.Machine) { 0x014c { "x86" } 0x8664 { "x64" } default { "unknown" } } if ($arch -ne $ExpectedArch) { throw "Architecture mismatch: expected $ExpectedArch, got $arch" } Write-Host "✅ Architecture: $arch" -ForegroundColor Green } catch { Write-Warning "Get-PEHeader not available; skipping arch check" } # 3. 检查导出函数(使用 dumpbin,需 VS 开发者命令提示符) $dumpbin = "${env:VSINSTALLDIR}VC\Tools\MSVC\*\bin\Hostx64\x64\dumpbin.exe" if (-not (Get-Command $dumpbin -ErrorAction SilentlyContinue)) { $dumpbin = "dumpbin.exe" # fallback to PATH } $exports = & $dumpbin /exports $DllPath 2>$null | Select-String "^\s+\d+\s+\d+\s+[0-9A-Fa-f]+\s+" if ($exports.Count -eq 0) { throw "No exports found — is this a managed DLL without UnmanagedExports?" } Write-Host "✅ Found $($exports.Count) exported functions" -ForegroundColor Green # 4. 检查 .NET 依赖(使用 ilspycmd) $ilspy = "ilspycmd" if (Get-Command $ilspy -ErrorAction SilentlyContinue) { $deps = & $ilspy --dependencies $DllPath 2>$null | Select-String "Microsoft\.|System\." if ($deps) { Write-Host "⚠️ .NET dependencies detected:" -ForegroundColor Yellow $deps | ForEach-Object { Write-Host " $($_.Line.Trim())" } } } # 5. C++ 加载测试(编译并运行微型测试 EXE) $testCpp = @" #include <windows.h> #include <stdio.h> int main() { HMODULE h = LoadLibrary(L"$($DllPath.Replace('\', '\\'))"); if (!h) { printf("Load failed: %lu\\n", GetLastError()); return 1; } printf("Load success\\n"); FreeLibrary(h); return 0; } "@ $testCppPath = Join-Path $env:TEMP "TestLoad.cpp" $testExePath = Join-Path $env:TEMP "TestLoad.exe" $testCpp | Out-File -FilePath $testCppPath -Encoding ASCII & cl /nologo /EHsc $testCppPath /Fe$testExePath /link user32.lib 2>$null if ($LASTEXITCODE -eq 0) { $result = & $testExePath 2>&1 if ($result -match "Load success") { Write-Host "✅ C++ LoadLibrary test passed" -ForegroundColor Green } else { throw "C++ test failed: $result" } } Write-Host "🎉 Validation complete. DLL is production-ready." -ForegroundColor Cyan运行方式:
.\Validate-Dll.ps1 -DllPath ".\MathCoreLib.dll" -ExpectedArch "x64"5.2 跨平台兼容性兜底:Linux/macOS 上的 .NET 6+ 替代方案
虽然 Windows 是 DLL 主战场,但若你的算法需跑在 Linux 服务器(如模型推理服务),.dll无法直接使用。此时应将核心逻辑抽离为.NET Standard 2.1 类库,再通过DllImport调用平台原生库(如 Linux 的.so),或直接用System.Runtime.InteropServices调用 POSIX API。
关键改造点:
- 将
MathCoreLib项目<TargetFramework>改为netstandard2.1; - 移除所有 Windows 专属 API(
System.Drawing,Microsoft.Win32); - 用
RuntimeInformation.IsOSPlatform(OSPlatform.Linux)做运行时分支; - 对于必须的原生计算,用
NativeLibrary.Load加载.so,而非LoadLibrary。
// CrossPlatformCalculator.cs public static class CrossPlatformCalculator { static CrossPlatformCalculator() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { _nativeLib = NativeLibrary.Load("MathCoreLib.dll"); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { _nativeLib = NativeLibrary.Load("libmathcore.so"); } } private static nint _nativeLib; [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })] public static int CalculateSqrt(double input, out double result) { // 实际跨平台实现 result = Math.Sqrt(input); return 0; } }从那以后我每次交付 DLL,都强制走一遍
Validate-Dll.ps1,再用dumpbin /exports和corflags双重确认。不是信不过自己,而是信不过 Windows 加载器那毫秒级的静默失败。希望帮到你。
本文还有配套的精品资源,点击获取