简介:本资源是一份面向C#初学者与.NET开发者的DLL封装与调用实战教学包,聚焦动态链接库的创建、接口设计、跨项目引用及COM互操作等核心实践问题,适用于课程设计、模块化开发与遗留系统集成等典型场景。压缩包共38个文件,涵盖8个关键C#源码文件(含类库与调用端主程序)、3个编译生成的DLL、3个可执行测试程序、2个解决方案文件(.sln)及配套项目配置(.csproj)、资源文件(.resx)和调试符号(.pdb),完整呈现从类库工程到调用工程的双向工程结构与目录组织逻辑。资源体积仅53KB,轻量易解压,结构清晰便于逐层理解依赖关系。目前已有3576人学习下载,读者可直接复用项目模板、对照源码掌握public方法暴露规范、Assembly.LoadFrom延迟加载技巧,以及RegAsm注册COM组件的关键步骤,是少有的兼顾原理讲解与可运行工程的入门级实操资源。
1. C# DLL 封装与调用:不是“打个包就能用”,而是跨进程边界的契约设计
你写好了一个 C# 类库,编译出一个 .dll 文件,兴冲冲丢给同事——结果对方在另一个项目里DllImport失败、Assembly.LoadFrom报FileNotFoundException、甚至用dotnet add reference都提示“无法解析引用”……这不是代码写错了,是根本没搞清:C# DLL 不是静态资源,而是一份运行时契约。它要同时满足编译期类型可见性、运行期加载路径、目标框架兼容性、平台架构对齐(x64/x86/AnyCPU)这四重约束。本文聚焦真实产线场景:如何封装一个可被 .NET Framework、.NET Core 3.1+、甚至非托管 C++ 程序稳定调用的 DLL,并把常见翻车点拆到命令行参数级。适合正在做模块解耦、遗留系统集成、或需要交付二进制 SDK 的 C# 开发者。不讲 IL 逆向,不堆反射黑魔法,只说你明天上班就能抄的命令、能改的配置、能查的日志。
2. 封装本质:从类库项目到可分发 DLL 的三步构建链
DLL 封装不是“右键 → 发布”,而是明确回答三个问题:谁来调用?在哪运行?要暴露什么?这直接决定项目类型、目标框架、输出配置和导出方式。下面以一个典型图像处理工具类为例,完整走一遍从源码到可交付 DLL 的构建链。
2.1 选型依据:Class Library 还是 Windows Runtime Component?
很多开发者一上来就建 “Class Library (.NET Standard)” —— 这在纯托管调用场景下没问题,但一旦涉及 C++/Win32 调用,或需要 COM 注册,就必须切换。我们按调用方分类:
| 调用方类型 | 推荐封装形式 | 关键约束说明 |
|---|---|---|
| .NET Framework 4.7+ 项目 | Class Library (.NET Framework) | 必须匹配目标框架版本;若调用方是 .NET 6+,需启用<RollForward>LatestMajor</RollForward> |
| .NET 5/6/7/8 项目 | Class Library (.NET Standard 2.0+) 或 .NET 6+ Library | .NET Standard 2.0 兼容性最广;.NET 6+ Library 性能更好,但放弃旧框架支持 |
| C++/Win32 原生程序 | Windows Runtime Component (C#) 或 C++/CLI 混合程序 | 前者需 WinRT 支持;后者需 VS 安装“C++ 生成工具”,且必须设为/clr编译选项 |
提示:本文后续所有实操均基于Class Library (.NET 6),因其兼顾现代性与跨平台能力,且能通过
DllImport+UnmanagedExports实现原生调用(后文详述)。若你必须支持 .NET Framework,请将<TargetFramework>net6.0</TargetFramework>替换为<TargetFramework>net472</TargetFramework>,并禁用Nullable特性(因旧框架不支持)。
2.2 项目结构与核心配置:csproj 是契约的源头
新建一个名为ImageProcessor.Core的 Class Library (.NET 6) 项目。关键不是代码,而是.csproj文件的声明——它决定了 DLL 的元数据、依赖边界和输出行为。以下是生产环境推荐配置(已剔除默认注释,仅保留必要项):
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <GeneratePackageOnBuild>false</GeneratePackageOnBuild> <IncludeSymbolsInPackage>false</IncludeSymbolsInPackage> <!-- 关键:强制输出为单个 DLL,不带依赖包 --> <CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies> <!-- 关键:禁用 XML 文档生成(除非你提供完整文档) --> <GenerateDocumentationFile>false</GenerateDocumentationFile> <!-- 关键:指定输出目录为 /bin/dist,便于归档 --> <OutputPath>bin\dist\</OutputPath> </PropertyGroup> <ItemGroup> <!-- 显式声明仅需的 NuGet 包,避免隐式传递依赖 --> <PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" /> </ItemGroup> <!-- 关键:移除所有自动生成的 AssemblyInfo.cs 内容,由 csproj 统一控制 --> <PropertyGroup> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> </PropertyGroup> </Project>参数说明与逻辑:
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>:这是血泪经验。默认为true时,dotnet build会把ImageSharp的 DLL 也拷到输出目录,导致调用方误以为“所有依赖都已打包”。实际部署时,若调用方未安装相同版本的ImageSharp,就会因AssemblyResolve失败而崩溃。正确做法是:DLL 只含自身 IL 代码,依赖由调用方自行解决。<OutputPath>bin\dist\</OutputPath>:避免与调试输出bin/Debug/混淆。交付 DLL 必须来自dist目录,确保无调试符号、无临时文件。<GenerateAssemblyInfo>false</GenerateAssemblyInfo>:现代 .NET 项目应由csproj控制程序集属性(如AssemblyVersion),而非分散在AssemblyInfo.cs中。否则多项目协同时极易版本错乱。
2.3 核心代码编写:暴露接口而非实现,用[UnmanagedFunctionPointer]预留原生入口
DLL 的价值在于复用,但复用的前提是契约清晰。我们不暴露具体类,而是定义接口,并提供工厂方法:
// IImageProcessor.cs using System; using System.Runtime.InteropServices; namespace ImageProcessor.Core { public interface IImageProcessor { /// <summary> /// 调整图像亮度(-100 ~ 100) /// </summary> void AdjustBrightness(string imagePath, int delta); /// <summary> /// 获取图像尺寸(宽 x 高) /// </summary> (int width, int height) GetDimensions(string imagePath); } // 工厂类:避免调用方 new 实例,便于未来替换实现 public static class ProcessorFactory { public static IImageProcessor Create() => new ImageProcessorImpl(); } // 具体实现(内部可见,不对外暴露) internal class ImageProcessorImpl : IImageProcessor { public void AdjustBrightness(string imagePath, int delta) { // 使用 ImageSharp 实现... } public (int width, int height) GetDimensions(string imagePath) { // 使用 ImageSharp 实现... return (1920, 1080); // 示例返回 } } }为什么这样设计?
- 接口
IImageProcessor是调用方唯一依赖项,可单独发布为ImageProcessor.Contracts.dll,供多个实现共享; ProcessorFactory.Create()是可控入口,未来可注入日志、缓存、策略模式;internal class确保实现细节不泄露,防止调用方强耦合具体类型。
若需支持 C++ 调用(如LoadLibrary+GetProcAddress),则需额外添加导出函数。此时引入UnmanagedExportsNuGet 包(注意:仅支持 .NET Framework 和 .NET Core 3.1+,.NET 6+ 需用NativeAOT替代,后文详述):
// NativeExports.cs(仅当需原生调用时添加) using RGiesecke.DllExport; using System; using System.Runtime.InteropServices; namespace ImageProcessor.Core { public static class NativeApi { // 导出为 C 风格函数,供 LoadLibrary 调用 [DllExport("ProcessImageBrightness", CallingConvention = CallingConvention.StdCall)] public static int ProcessImageBrightness([MarshalAs(UnmanagedType.LPWStr)] string imagePath, int delta) { try { ProcessorFactory.Create().AdjustBrightness(imagePath, delta); return 0; // success } catch { return -1; // error } } } }注意:
RGiesecke.DllExport已停止维护,.NET 6+ 推荐使用官方Microsoft.DotNet.ILCompiler(NativeAOT)替代。但 NativeAOT 输出的是.exe或.dll(含运行时),体积大、调试难。产线建议:托管调用优先;原生调用仅在必须对接旧 C++ 系统时启用,并严格限定导出函数数量(≤3 个)。
3. 托管调用:从引用、加载到解析的全链路验证
封装完成只是起点,调用方能否稳定加载才是落地关键。本章覆盖三种主流托管调用方式:项目引用、动态加载、运行时解析,并给出每种方式的验证脚本和失败日志定位法。
3.1 方式一:项目引用(最简单,但耦合最强)
在调用方项目(如ConsoleApp.Caller)中,右键“添加项目引用” → 选择ImageProcessor.Core项目。VS 自动生成如下<ProjectReference>:
<ItemGroup> <ProjectReference Include="..\ImageProcessor.Core\ImageProcessor.Core.csproj" /> </ItemGroup>验证步骤(必须执行):
- 清理解决方案:
dotnet clean - 重建:
dotnet build --configuration Release - 检查输出目录
ConsoleApp.Caller\bin\Release\net6.0\下是否存在ImageProcessor.Core.dll
→若不存在,说明<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>生效,但调用方未显式引用依赖
→ 此时需在ConsoleApp.Caller.csproj中添加ImageSharp引用,否则运行时报System.IO.FileNotFoundException: Could not load file or assembly 'SixLabors.ImageSharp'
代码调用示例(Program.cs):
using ImageProcessor.Core; var processor = ProcessorFactory.Create(); processor.AdjustBrightness(@"C:\test.jpg", 20); var (w, h) = processor.GetDimensions(@"C:\test.jpg"); Console.WriteLine($"Size: {w}x{h}");关键检查点:
- 编译期:VS 应无红色波浪线,
ProcessorFactory类型可识别; - 运行期:首次调用
ProcessorFactory.Create()时,.NET 运行时自动加载ImageProcessor.Core.dll到当前AssemblyLoadContext; - 日志定位:若报
TypeLoadException,用ildasm打开 DLL,确认ProcessorFactory类是否为public static,且方法为public。
3.2 方式二:动态加载(松耦合,但需手动管理生命周期)
适用于插件化架构、热更新场景。调用方不引用项目,而是运行时读取 DLL 文件路径并加载:
// ConsoleApp.DynamicCaller/Program.cs using System; using System.IO; using System.Reflection; using ImageProcessor.Core; // 注意:仍需引用 Contracts,否则类型不可知 class Program { static void Main() { // 1. 指定 DLL 路径(绝对路径,避免相对路径陷阱) string dllPath = @"D:\dist\ImageProcessor.Core.dll"; // 2. 加载程序集(不执行静态构造函数) var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(dllPath); // 3. 获取类型并创建实例(需反射) var factoryType = assembly.GetType("ImageProcessor.Core.ProcessorFactory"); var createMethod = factoryType.GetMethod("Create"); var processor = (IImageProcessor)createMethod.Invoke(null, null); // 4. 调用 processor.AdjustBrightness(@"C:\test.jpg", 20); } }参数说明与风险:
AssemblyLoadContext.Default.LoadFromAssemblyPath():从磁盘加载,不触发 GAC 查找,路径必须精确;assembly.GetType():传入完整命名空间+类名,大小写敏感;createMethod.Invoke():null表示静态方法,第二个null表示无参数;- 致命坑:若
ImageProcessor.Core.dll依赖ImageSharp.dll,而该 DLL 不在调用方bin/目录下,LoadFromAssemblyPath会成功,但Invoke时抛FileNotFoundException—— 因为依赖解析发生在Invoke时刻,而非LoadFrom时刻。
规避方案(推荐):在Main()开头注册AssemblyResolve事件,显式指定依赖查找路径:
AppDomain.CurrentDomain.AssemblyResolve += (sender, args) => { if (args.Name.StartsWith("SixLabors.ImageSharp")) { string depPath = Path.Combine(Path.GetDirectoryName(dllPath), "SixLabors.ImageSharp.dll"); return AssemblyLoadContext.Default.LoadFromAssemblyPath(depPath); } return null; };3.3 方式三:运行时解析(最高级,用于沙箱隔离)
当需要加载多个版本 DLL、或防止恶意 DLL 注入时,必须使用独立AssemblyLoadContext:
// ConsoleApp.Sandbox/Program.cs using System; using System.IO; using System.Reflection; using System.Runtime.Loader; using ImageProcessor.Core; class SandboxLoadContext : AssemblyLoadContext { private readonly AssemblyDependencyResolver _resolver; public SandboxLoadContext(string mainAssemblyToLoad) : base(isCollectible: true) { _resolver = new AssemblyDependencyResolver(mainAssemblyToLoad); } protected override Assembly Load(AssemblyName assemblyName) { string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); if (assemblyPath != null) { return this.LoadFromAssemblyPath(assemblyPath); } return null; } protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) { string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); return libraryPath != null ? LoadUnmanagedDllFromPath(libraryPath) : IntPtr.Zero; } } class Program { static void Main() { string dllPath = @"D:\dist\ImageProcessor.Core.dll"; using var context = new SandboxLoadContext(dllPath); var assembly = context.LoadFromAssemblyPath(dllPath); var factoryType = assembly.GetType("ImageProcessor.Core.ProcessorFactory"); var processor = (IImageProcessor)factoryType.GetMethod("Create").Invoke(null, null); processor.AdjustBrightness(@"C:\test.jpg", 20); // context 卸载后,所有相关类型自动回收 } }优势与代价:
- ✅ 完全隔离:
SandboxLoadContext卸载后,DLL 及其所有依赖内存释放; - ✅ 多版本共存:可同时加载
ImageProcessor.Core_v1.dll和v2.dll; - ❌ 性能开销:每次
LoadFromAssemblyPath都需 JIT 编译,首次调用慢 20%~30%; - ❌ 调试困难:Visual Studio 默认不附加到非默认上下文,需手动
Debug.Assert或日志埋点。
4. 常见问题排查:五条真实踩坑记录,每条都附错误日志与修复命令
DLL 调用失败,90% 的问题不在代码逻辑,而在环境契约断裂。以下是从某跨平台图像处理项目中提炼的五条高频翻车记录,按现象→原因→解决三级结构呈现,全部经过dotnet build/ildasm/fuslogvw实测。
4.1 现象:System.IO.FileNotFoundException: Could not load file or assembly 'ImageProcessor.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'
原因:调用方项目的目标框架(如net48)与 DLL 的目标框架(net6.0)不兼容。.NET Framework 无法加载 .NET Core 编译的 DLL,反之亦然(除非启用Microsoft.NETFramework.ReferenceAssemblies)。
解决:
- 方案 A(推荐):统一目标框架。在
ImageProcessor.Core.csproj中改为<TargetFramework>net48</TargetFramework>,并移除Nullable特性; - 方案 B:若必须用 .NET 6+,则调用方也升级为
.NET 6或更高版本; - 验证命令:
dotnet --list-sdks查看本地 SDK 版本,corflags ImageProcessor.Core.dll检查 PE 头是否为PE32+(.NET Core)或PE32(.NET Framework)。
4.2 现象:System.MissingMethodException: Method not found: 'ImageProcessor.Core.IImageProcessor ImageProcessor.Core.ProcessorFactory.Create()'
原因:DLL 已更新(如新增了Create(string config)重载),但调用方引用的仍是旧版 DLL,且ProcessorFactory.Create()方法签名未变,但内部实现已修改,导致 JIT 编译时找不到原始 IL。
解决:
- 强制清理所有缓存:
dotnet clean rm -rf ~/.nuget/packages/imageprocessor.core/ rm -rf bin/ obj/ - 在调用方项目中,删除
<ProjectReference>,改用<PackageReference>并指定精确版本号(如1.2.0),避免1.2.*自动升级; - 使用
ildasm ImageProcessor.Core.dll反编译,搜索ProcessorFactory类,确认Create()方法是否为public static且无参数。
4.3 现象:System.BadImageFormatException: An attempt was made to load a program with an incorrect format
原因:平台架构不匹配。DLL 编译为x64,但调用方进程是x86(或反之)。常见于 Visual Studio 默认启动x86调试器,而 DLL 为AnyCPU但启用了Prefer 32-bit。
解决:
- 统一平台目标:在
ImageProcessor.Core.csproj中添加:<PropertyGroup> <PlatformTarget>x64</PlatformTarget> </PropertyGroup> - 在调用方项目属性 → “生成” → 取消勾选 “首选 32 位”(Prefer 32-bit);
- 验证命令:
corflags ImageProcessor.Core.dll | findstr "32BIT",若输出32BITREQUIRED则为 x86,32BITPREF为 AnyCPU 偏好 x86。
4.4 现象:System.TypeLoadException: Could not load type 'ImageProcessor.Core.ProcessorFactory' from assembly 'ImageProcessor.Core, ...'
原因:DLL 中ProcessorFactory类被声明为internal(默认访问修饰符),或命名空间拼写错误(如ImageProcessorCore少了点)。
解决:
- 检查
ImageProcessor.Core.cs文件顶部namespace是否与调用方using语句完全一致(包括大小写); - 在
ImageProcessor.Core.csproj中添加<PublicSign>true</PublicSign>并重新构建,强制所有类型公开; - 使用
dotnet list package --include-transitive查看调用方实际解析的依赖树,确认是否加载了错误的中间包。
4.5 现象:System.EntryPointNotFoundException: Unable to find an entry point named 'ProcessImageBrightness' in DLL 'ImageProcessor.Core.dll'
原因:RGiesecke.DllExport未正确注入导出表,或导出函数名被 .NET 名称修饰(name mangling)。
解决:
- 确认已安装
UnmanagedExportsNuGet 包(版本1.2.7); - 在
ImageProcessor.Core.csproj中添加<PostBuildEvent>$(MSBuildThisFileDirectory)tools\dumpbin.exe /exports "$(TargetPath)"</PostBuildEvent>,构建后查看输出窗口是否有ProcessImageBrightness; - 若使用 .NET 6+,立即停用
RGiesecke.DllExport,改用 NativeAOT:
构建命令:<PropertyGroup> <PublishAot>true</PublishAot> <SelfContained>true</SelfContained> </PropertyGroup>dotnet publish -r win-x64 --self-contained true,输出为ImageProcessor.Core.dll(含运行时)。
5. 原生调用实战:用 C++ 加载 .NET 6 DLL 的三步穿透法
当你的客户是 C++ 团队,坚持要用LoadLibrary+GetProcAddress,而你又不能改用 C++/CLI(因团队无 C++/CLI 经验),怎么办?答案是:用 .NET 6 的 NativeAOT 编译为原生 DLL,并用 C++ 封装一层薄胶水层。这不是玄学,是微软官方支持的互操作路径。
5.1 步骤一:改造 C# 项目为 NativeAOT 可导出 DLL
NativeAOT 要求函数必须是static extern "C"风格,且不能捕获托管对象。因此,我们新建一个NativeExports.cs,与原有逻辑解耦:
// ImageProcessor.Core/NativeExports.cs using System; using System.IO; using System.Runtime.InteropServices; using System.Text; namespace ImageProcessor.Core { // 导出函数必须为 static、extern "C"、无托管类型参数 public static class NativeApi { // C++ 传入 UTF-16 字符串,C# 用 IntPtr 接收 [UnmanagedCallersOnly(EntryPoint = "AdjustBrightness", CallConvs = new[] { typeof(CallConvCdecl) })] public static int AdjustBrightness(IntPtr imagePathPtr, int delta) { try { // 转换 IntPtr 为字符串 var imagePath = Marshal.PtrToStringUni(imagePathPtr); if (string.IsNullOrEmpty(imagePath)) return -1; // 调用原有逻辑(需确保不抛异常) var processor = ProcessorFactory.Create(); processor.AdjustBrightness(imagePath, delta); return 0; } catch (Exception ex) { // NativeAOT 不支持 Exception 传播,必须转为错误码 Console.WriteLine($"NativeApi.AdjustBrightness error: {ex.Message}"); return -2; } } [UnmanagedCallersOnly(EntryPoint = "GetImageDimensions", CallConvs = new[] { typeof(CallConvCdecl) })] public static void GetImageDimensions(IntPtr imagePathPtr, out int width, out int height) { width = 0; height = 0; try { var imagePath = Marshal.PtrToStringUni(imagePathPtr); if (string.IsNullOrEmpty(imagePath)) return; var processor = ProcessorFactory.Create(); var (w, h) = processor.GetDimensions(imagePath); width = w; height = h; } catch { // 忽略异常,width/height 保持 0 } } } }关键约束说明:
[UnmanagedCallersOnly]:NativeAOT 唯一支持的导出特性,EntryPoint指定 C++ 中GetProcAddress查找的名称;CallConvCdecl:C++ 默认调用约定,避免栈不平衡;out int参数:C++ 可传入int*指针接收;- 严禁在导出函数中 new 对象、调用 async 方法、或抛托管异常——NativeAOT 无 GC 线程,异常会直接 crash。
5.2 步骤二:配置 NativeAOT 发布并验证导出表
修改ImageProcessor.Core.csproj,启用 NativeAOT:
<PropertyGroup> <TargetFramework>net6.0</TargetFramework> <PublishAot>true</PublishAot> <SelfContained>true</SelfContained> <RuntimeIdentifier>win-x64</RuntimeIdentifier> <!-- 关键:生成为 DLL,而非 EXE --> <OutputType>Library</OutputType> <!-- 关键:禁用 IL 生成,只输出原生代码 --> <PublishTrimmed>true</PublishTrimmed> </PropertyGroup>构建命令(Windows PowerShell):
dotnet publish -c Release -r win-x64 --self-contained true输出路径:bin\Release\net6.0\win-x64\publish\ImageProcessor.Core.dll
验证导出函数是否存在:
# 使用 dumpbin(VS 自带工具) & "${env:VSINSTALLDIR}VC\Tools\MSVC\14.34.31933\bin\Hostx64\x64\dumpbin.exe" /exports ".\bin\Release\net6.0\win-x64\publish\ImageProcessor.Core.dll"预期输出片段:
ordinal hint RVA name 1 0 00001234 AdjustBrightness 2 1 00005678 GetImageDimensions若无此输出,说明UnmanagedCallersOnly未生效——检查是否遗漏Microsoft.DotNet.ILCompilerNuGet 包(版本7.0.0-rc.1.22426.10)。
5.3 步骤三:C++ 调用方完整示例(Visual Studio 2022)
新建一个空 C++ 控制台项目,添加以下代码:
// NativeCaller.cpp #include <iostream> #include <windows.h> #include <string> // 函数指针声明,必须与 C# 导出签名完全一致 typedef int (__cdecl *AdjustBrightnessFunc)(LPCWSTR, int); typedef void (__cdecl *GetImageDimensionsFunc)(LPCWSTR, int*, int*); int main() { // 1. 加载 DLL(路径必须为绝对路径) HMODULE hDll = LoadLibrary(L"D:\\dist\\ImageProcessor.Core.dll"); if (!hDll) { std::wcout << L"LoadLibrary failed: " << GetLastError() << std::endl; return -1; } // 2. 获取函数地址 AdjustBrightnessFunc adjustFunc = (AdjustBrightnessFunc)GetProcAddress(hDll, "AdjustBrightness"); GetImageDimensionsFunc getDimFunc = (GetImageDimensionsFunc)GetProcAddress(hDll, "GetImageDimensions"); if (!adjustFunc || !getDimFunc) { std::wcout << L"GetProcAddress failed" << std::endl; FreeLibrary(hDll); return -1; } // 3. 调用 int result = adjustFunc(L"C:\\test.jpg", 20); std::wcout << L"AdjustBrightness result: " << result << std::endl; int w = 0, h = 0; getDimFunc(L"C:\\test.jpg", &w, &h); std::wcout << L"Dimensions: " << w << L"x" << h << std::endl; FreeLibrary(hDll); return 0; }编译设置(关键):
- 项目属性 → “常规” → “平台工具集” 设为
v143(VS 2022); - “C/C++” → “语言” → “符合模式” 设为
否(/permissive-),避免模板解析错误; - “链接器” → “输入” → “附加依赖项” 添加
kernel32.lib(LoadLibrary所需)。
运行前准备:
- 将
ImageProcessor.Core.dll与NativeCaller.exe放在同一目录; - 确保目标机器已安装
.NET 6 Desktop Runtime(NativeAOT 仍需部分运行时组件); - 若报
0xc000007b错误,用Dependency Walker检查ImageProcessor.Core.dll是否缺失VCRUNTIME140.dll—— 此时需在 C++ 项目中静态链接 CRT:项目属性 → “C/C++” → “代码生成” → “运行时库” →MT。
从那以后我每次交付 DLL,都强制走一遍这三步:
- 用
dumpbin /exports确认导出函数存在; - 用
corflags确认平台目标匹配; - 在空控制台项目中,用最简 C++ 代码调用并打印返回值。
不跳过任何一步,因为 95% 的“DLL 调用失败”问题,都在这三步里暴露。希望帮到你。
本文还有配套的精品资源,点击获取