C#与C++互操作实战:P/Invoke调用DLL完整指南
2026/7/19 10:25:56 网站建设 项目流程

1. 项目概述:为什么我们需要C#与C++的互操作?

在桌面应用、工业控制、游戏开发或者高性能计算领域,我们常常会遇到一个经典的“组合拳”场景:用C#快速构建优雅的用户界面和业务逻辑,同时,为了榨干硬件的最后一丝性能,或者复用那些历经考验的古老而强大的C/C++代码库,我们需要在后台调用C++编写的核心算法或驱动。这时,一个名为“DLL”(动态链接库)的桥梁就变得至关重要。这个项目,就是一份关于如何架设这座桥梁的完整实战指南。

简单来说,当你的C#程序需要调用一个用C++编译好的.dll文件中的函数时,你不能像调用C#自己的类库那样直接new一个对象。因为两者身处不同的“世界”:C#运行在托管环境(.NET CLR)中,内存自动管理,安全可控;而C++通常是本机代码,直接操作内存,生死自负。让它们安全、正确、高效地对话,就是“互操作”(Interoperability,常简写为Interop)要解决的核心问题。

我见过太多项目在这里栽跟头:程序运行时莫名崩溃,返回的数据乱码,或者性能还不如纯C#重写。这些问题,十有八九出在互操作的细节上——数据类型如何映射、内存由谁管理、异常如何传递。这份指南的目的,就是带你穿越这些雷区,从最简单的函数调用,到复杂的结构体和回调函数传递,手把手构建一个健壮、可维护的C#与C++ DLL互操作方案。无论你是需要集成一个第三方的C++ SDK,还是将自己团队积累的C++核心模块暴露给新的C#前端,这篇文章都能给你一套可直接复用的“脚手架”。

2. 核心原理与方案选型:P/Invoke与C++/CLI的抉择

实现C#调用C++,主流有两大技术路径:P/Invoke(平台调用)和C++/CLI。选择哪一种,取决于你的具体场景、对性能和控制力的要求,以及团队的技能栈。

2.1 P/Invoke:轻量级直接调用

P/Invoke(Platform Invocation Services)是.NET框架提供的基础机制。它的工作方式非常直接:你在C#代码中,使用[DllImport]特性声明一个外部函数,指明它来自哪个DLL,然后就可以像调用普通静态方法一样使用它。

using System.Runtime.InteropServices; public class NativeMethods { [DllImport("MyNativeLib.dll", EntryPoint = "calculate_sum")] public static extern int CalculateSum(int a, int b); }

它的核心优势在于“轻”

  1. 无需中间层:C#直接与原生DLL对话,没有额外的包装或运行时开销。
  2. 部署简单:只需要将C++编译好的DLL文件放在应用程序的执行目录(或系统路径)下即可。
  3. 技术门槛相对较低:你只需要了解如何在C#端正确声明函数签名,无需学习另一种混合语言。

但它也有明显的“约束”

  1. “平面C”接口限制:P/Invoke最擅长调用的是使用C语言调用约定(extern "C")导出的函数。它处理的是简单的函数、基本数据类型和结构体。对于复杂的C++类、模板、重载运算符等面向对象特性,P/Invoke无能为力。
  2. 手动内存管理:当传递字符串、数组或结构体指针时,你需要仔细处理内存的分配和释放,特别是在跨边界传递时,稍有不慎就会导致内存泄漏或访问冲突。
  3. 错误处理不便:原生代码通常通过返回值或设置全局错误码来表示错误,而.NET使用异常机制。你需要手动将这两种错误处理方式转换。

注意:P/Invoke是处理已有、稳定的C语言风格接口库的首选。如果你的C++代码本身就很“C”(即大量使用全局函数和结构体),或者你调用的是操作系统API、老牌第三方库(如许多硬件驱动SDK),P/Invoke是最直接高效的方案。

2.2 C++/CLI:强大的混合模式桥梁

C++/CLI是一种特殊的C++方言,它允许你在同一个项目、甚至同一个文件中编写原生C++代码和托管.NET代码。你可以用它创建一个特殊的“桥接DLL”(通常编译为混合程序集,.dll但包含.NET元数据)。

在这个桥接DLL中,你可以:

  • 编写原生C++类来实现核心逻辑。
  • 编写特殊的“包装类”(使用ref class关键字),这些类既能在内部与原生C++对象交互,又能以完全.NET友好的方式(属性、方法、事件、继承)暴露给C#。
  • 由C++/CLI运行时自动处理托管与非托管内存之间的转换(通过marshal机制)。

它的核心优势在于“强”和“自然”

  1. 完美封装C++对象:可以将整个C++类包装成一个.NET类,C#端可以new对象、调用方法、处理事件,几乎感觉不到底层是C++。
  2. 自动化的内存与类型转换:C++/CLI编译器提供了强大的marshal_as等工具,能自动处理std::stringSystem::String这类复杂转换。
  3. 双向交互:不仅C#能调用C++,C++/CLI代码也能方便地回调C#委托(相当于函数指针),实现更复杂的交互模式。
  4. 更好的调试体验:在Visual Studio中,可以同时在托管代码和原生代码中单步调试。

它的代价是“重”

  1. 引入新的语言和编译模型:开发团队需要学习C++/CLI的语法和特性。
  2. 部署依赖:生成的混合程序集需要目标机器安装对应版本的.NET Framework和VC++运行时。
  3. 项目结构更复杂:需要维护一个额外的C++/CLI桥接项目。

实操心得:对于全新的、需要深度集成的项目,或者你需要暴露一个复杂的、面向对象的C++库给多个.NET语言(C#、VB.NET等)使用,投资建立一个C++/CLI包装层是值得的。它能提供最优雅、最类型安全的API。但对于一次性、简单的函数调用,P/Invoke的快捷性无可替代。

本指南的侧重点:鉴于P/Invoke应用更广泛、更基础,且是理解互操作根本原理的钥匙,下文将主要围绕P/Invoke展开,详细拆解其实现中的每一个核心环节和坑点。掌握了P/Invoke,你也能更容易地理解C++/CLI所解决的问题。

3. 实战P/Invoke:从声明到调用的完整流程

让我们从一个最简单的例子开始,逐步增加复杂度,构建一个完整的互操作场景。假设我们有一个用C++编写的数学库MathLib.dll

3.1 第一步:准备C++ DLL

首先,我们需要确保C++ DLL以正确的方式导出函数。这是所有互操作的基础。

(1)使用extern "C"防止名称粉碎(Name Mangling)C++编译器为了支持函数重载,会对函数名进行修饰(粉碎),例如Calculate可能变成?Calculate@@YAHHH@Z。P/Invoke无法识别这种名字。extern "C"告诉编译器使用C语言的命名和调用约定。

(2)明确指定调用约定最常见的是__stdcall(Windows API标准)和__cdecl(C/C++默认)。必须与C#端的声明一致。通常,如果C++代码中没有指定,默认是__cdecl

(3)使用__declspec(dllexport)导出函数一个标准的导出头文件MathLib.h可能如下所示:

// MathLib.h #pragma once #ifdef MATHLIB_EXPORTS #define MATHLIB_API __declspec(dllexport) #else #define MATHLIB_API __declspec(dllimport) #endif // 使用extern "C"确保C语言链接,使用__cdecl调用约定(也可指定__stdcall) extern "C" { MATHLIB_API int __cdecl Add(int a, int b); MATHLIB_API double __cdecl Multiply(double a, double b); }

对应的源文件MathLib.cpp

// MathLib.cpp #include "MathLib.h" int __cdecl Add(int a, int b) { return a + b; } double __cdecl Multiply(double a, double b) { return a * b; }

在Visual Studio中创建DLL项目时,需要在项目属性预处理器定义中添加MATHLIB_EXPORTS,这样编译时MATHLIB_API才会展开为__declspec(dllexport)

3.2 第二步:在C#中声明与调用

在C#项目中,我们首先需要将编译好的MathLib.dll复制到输出目录(如bin\Debug)。然后,创建一个静态类来声明这些原生函数。

using System.Runtime.InteropServices; namespace InteropGuide { public static class NativeMath { // 声明Add函数 // DllImport特性指定DLL名称。EntryPoint可以省略,默认使用方法名。 // CallingConvention必须与C++端一致,这里是Cdecl。 [DllImport("MathLib.dll", EntryPoint = "Add", CallingConvention = CallingConvention.Cdecl)] public static extern int Add(int a, int b); [DllImport("MathLib.dll", CallingConvention = CallingConvention.Cdecl)] public static extern double Multiply(double a, double b); } class Program { static void Main(string[] args) { int sum = NativeMath.Add(5, 3); Console.WriteLine($"5 + 3 = {sum}"); double product = NativeMath.Multiply(4.5, 2.0); Console.WriteLine($"4.5 * 2.0 = {product}"); } } }

如果一切顺利,运行程序将得到正确结果。但这只是最简单的开始。真正的挑战在于处理更复杂的数据类型。

4. 复杂数据类型映射与内存管理详解

当函数参数或返回值不再是简单的intdouble,而是涉及字符串、数组、结构体甚至指针的指针时,互操作就进入了深水区。这里的关键是“封送处理”(Marshaling),即.NET运行时在托管内存和非托管内存之间转换数据的过程。

4.1 字符串的传递:最易出错的环节

C++中字符串可能是char*(ANSI)、wchar_t*(Unicode),也可能是std::string。C#中是string(Unicode)。类型不匹配会导致乱码或崩溃。

场景一:C#传递字符串给C++(C++修改或不修改)

  • C++函数void SetName(const char* name);(C++只读字符串)

  • C#声明

    [DllImport("MyLib.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] public static extern void SetName(string name);

    CharSet = CharSet.Ansi告诉封送拆收器将C#的Unicode字符串转换为ANSI字符串(char*)。如果C++端是wchar_t*,则使用CharSet = CharSet.Unicode。对于const参数,使用string类型是安全的,封送拆收器会自动分配临时内存并复制内容。

  • C++函数void GetBuffer(char* buffer, int bufferSize);(C++需要写入缓冲区)

  • C#声明

    [DllImport("MyLib.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] public static extern void GetBuffer(StringBuilder buffer, int bufferSize);

    这里必须使用StringBuilderString在.NET中是不可变的。StringBuilder内部有一个字符缓冲区,封送拆收器可以将C++写入的数据直接填充到这个缓冲区中。你需要预先分配好足够大小的StringBuilder

    StringBuilder buffer = new StringBuilder(256); // 分配256字符的缓冲区 GetBuffer(buffer, buffer.Capacity); string result = buffer.ToString();

场景二:C++返回字符串给C#

  • C++函数const char* GetName();(返回指向内部静态或全局内存的指针)
    • 危险!如果返回的是栈上数组的指针,函数返回后内存即失效,会导致未定义行为。
  • C#声明
    [DllImport("MyLib.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] [return: MarshalAs(UnmanagedType.LPStr)] // 明确指定返回类型是ANSI字符串指针 public static extern string GetName();
    .NET会复制返回的指针所指向的字符串内容到一个新的string对象中。这要求C++返回的指针在调用后的一段时间内必须保持有效(例如指向全局常量或堆内存)。

重要注意事项:对于C++返回的、需要由调用者释放的内存(例如通过new char[]分配),绝对不能直接用string接收。因为.NET无法知道需要用delete[]来释放这块内存。正确的做法是声明返回类型为IntPtr,然后使用Marshal.PtrToStringAnsi(ptr)复制字符串,最后调用C++提供的释放函数(如FreeMemory(IntPtr ptr))或使用Marshal.FreeCoTaskMem(ptr)(如果内存是用CoTaskMemAlloc分配的)。

4.2 结构体的传递:内存布局必须一致

传递结构体时,托管和非托管两端的内存布局必须完全一致,包括字段顺序、大小和对齐方式。

C++结构体

#pragma pack(push, 1) // 设置1字节对齐,避免编译器插入填充字节,确保布局可控 struct Point { int x; int y; char name[32]; }; #pragma pack(pop) // 恢复默认对齐

对应的C#结构体

[StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Ansi)] public struct Point { public int x; public int y; // 使用固定大小的字符数组来对应C++的char name[32] [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string name; }
  • LayoutKind.Sequential:强制字段按声明顺序排列。
  • Pack = 1:指定1字节对齐,与C++端的#pragma pack(1)对应。
  • [MarshalAs(...)]:指定name字段是一个内联的、固定大小的ANSI字符串。

传递方式

  • 传值void UsePoint(Point p);在C#中对应public static extern void UsePoint(Point p);。整个结构体被复制到非托管堆。
  • 传指针/引用void UsePoint(Point* p);void UsePoint(Point& p);在C#中对应public static extern void UsePoint(ref Point p);public static extern void UsePoint(IntPtr p);。使用ref更安全方便,封送拆收器会处理指针的传递。

4.3 数组与指针的传递

传递数组本质上是传递指向数组首元素的指针。

C++函数double SumArray(const double* arr, int length);

C#声明

[DllImport("MyLib.dll", CallingConvention = CallingConvention.Cdecl)] public static extern double SumArray(double[] arr, int length); // 或者更明确的指针形式 [DllImport("MyLib.dll", CallingConvention = CallingConvention.Cdecl)] public static extern double SumArray(IntPtr arr, int length);

使用double[]时,封送拆收器会自动将托管数组的内容复制到非托管内存(如果函数是只读的,使用[In]特性可以提示优化),函数返回后,如果函数修改了数组内容,封送拆收器还会将数据复制回来(如果使用了[In, Out]特性)。

对于需要由C++分配并返回给C#的数组,处理方式类似于返回的字符串指针:C#端声明返回IntPtr,然后使用Marshal.Copy(ptr, managedArray, startIndex, length)将数据复制到托管数组,最后释放内存。

4.4 回调函数(函数指针)的传递

这是实现双向通信的关键。C++端定义一个函数指针类型,C#端则用委托(Delegate)来对应。

C++端

typedef void (*LogCallback)(const char* message); extern "C" { void SetLogger(LogCallback callback); }

C#端

// 1. 定义与C函数指针匹配的委托 [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)] public delegate void LogCallbackDelegate(string message); // 2. 声明外部函数 [DllImport("MyLib.dll", CallingConvention = CallingConvention.Cdecl)] public static extern void SetLogger(LogCallbackDelegate callback); // 3. 实现一个符合委托签名的方法 static void MyLogger(string msg) { Console.WriteLine($"[Native Log] {msg}"); } // 4. 在C#中设置回调 static void Main() { LogCallbackDelegate callback = new LogCallbackDelegate(MyLogger); SetLogger(callback); // 必须保持委托实例的引用,防止被GC回收! }

关键点

  1. 必须使用[UnmanagedFunctionPointer]特性修饰委托,并指定正确的调用约定和字符集。
  2. 必须将委托实例保存在一个不会被垃圾回收的变量中(如类静态字段)。因为P/Invoke传递给C++的只是一个函数指针,.NET的GC不知道C++还在引用这个委托。如果委托实例被回收,C++再调用该指针会导致灾难性崩溃。

5. 高级主题与性能优化

5.1 使用SafeHandle封装非托管资源

如果你的C++函数返回一个句柄(如文件句柄、设备上下文句柄HANDLE、或指向复杂对象的指针void*),在C#中最好使用SafeHandle或其派生类(如SafeFileHandle)来封装它。SafeHandle能确保在对象被垃圾回收或程序异常终止时,非托管资源能被可靠地释放。

public sealed class MyNativeHandle : SafeHandleZeroOrMinusOneIsInvalid { // 构造函数,调用基类构造函数 private MyNativeHandle() : base(true) { } [DllImport("MyLib.dll")] private static extern MyNativeHandle CreateResource(); [DllImport("MyLib.dll")] private static extern void DestroyResource(IntPtr handle); // 重写ReleaseHandle方法,在这里释放非托管资源 protected override bool ReleaseHandle() { DestroyResource(handle); return true; // 返回true表示释放成功 } // 提供一个静态方法创建句柄 public static MyNativeHandle Create() { MyNativeHandle handle = CreateResource(); if (handle.IsInvalid) { throw new InvalidOperationException("Failed to create native resource."); } return handle; } }

5.2 控制封送处理行为以提升性能

默认的封送处理(如传递数组)会复制数据。对于频繁调用或大数据块,这会成为性能瓶颈。

  • [In][Out]特性:明确指定数据流向。如果数据只从C#传入C++,标记为[In],可以避免不必要的回拷。如果C++会修改数据并希望C#看到结果,标记为[In, Out]

    [DllImport("MyLib.dll")] public static extern void ProcessData([In, Out] byte[] data, int length);
  • fixed语句与指针:在unsafe上下文中,可以使用fixed语句固定托管数组在内存中的位置,直接获取指针传递给C++,实现零拷贝。这是最高效的方式,但也最危险,因为在fixed块内,GC无法移动该数组,如果C++长时间持有指针或在异步操作中使用此指针,可能导致问题。

    unsafe { byte[] largeData = new byte[1000000]; fixed (byte* pData = largeData) { ProcessDataRaw((IntPtr)pData, largeData.Length); } }

5.3 处理C++异常

P/Invoke本身无法直接捕获C++抛出的异常。如果C++函数抛出异常并跨越DLL边界,通常会导致程序崩溃。有几种处理方式:

  1. C++端捕获并转换为错误码:这是最安全、最通用的做法。在C++导出函数内部用try...catch捕获所有异常,并通过返回值、输出参数或设置错误码的方式将错误信息传递回C#。
  2. 使用SetErrorMode或结构化异常处理(SEH):对于访问违规等严重错误,可以配置Windows的错误处理方式,但这属于非常底层的操作,一般不建议。
  3. 使用C++/CLI:在C++/CLI包装层中,可以将原生C++异常转换为.NET的System.Exception,这样C#端就能用try-catch正常捕获了。

6. 常见问题、调试技巧与避坑指南

6.1 “找不到DLL”或“无法加载DLL”

  • 症状DllNotFoundExceptionBadImageFormatException
  • 排查
    1. 路径问题:确保DLL文件在应用程序的基目录、PATH环境变量包含的目录,或者通过SetDllDirectoryAPI设置的目录中。最简单的方法是将DLL复制到bin\Debugbin\Release下。
    2. 位数不匹配:这是最常见的原因!32位(x86)进程只能加载32位DLL,64位(x64)进程只能加载64位DLL。检查你的C#项目平台目标(Any CPU, x86, x64)和C++ DLL的编译平台是否一致。对于Any CPU,在32位系统上以x86运行,在64位系统上以x64运行。如果DLL只有32位,则必须将C#项目目标显式设置为x86
    3. 依赖缺失:使用Dependency WalkerVisual Studiodumpbin /dependents MyLib.dll命令检查你的DLL是否依赖其他DLL(如特定的VC++运行时库msvcrXXX.dll),并确保这些依赖在目标机器上可用。通常需要安装对应版本的Microsoft Visual C++ Redistributable

6.2 “尝试读取或写入受保护的内存”

  • 症状AccessViolationException
  • 原因:几乎总是内存管理错误。
    1. 缓冲区溢出:C#传递给C++的缓冲区(如StringBuilder)太小,C++写入了超出边界的内存。
    2. 悬空指针:C#传递了一个托管对象的引用(如数组),但在C++执行过程中,GC可能移动或回收了该对象。使用fixed语句或GCHandle.Alloc(obj, GCHandleType.Pinned)可以固定对象。
    3. 错误的指针类型:在C#端使用了错误的类型声明,导致封送拆收器对指针的解释错误。
    4. C++端内存错误:C++代码本身存在越界访问、使用已释放内存等问题。

6.3 调试技巧

  1. 启用本机代码调试:在C#项目的属性 -> 调试 -> 调试器类型中,勾选“启用本机代码调试”。这样你可以在Visual Studio中同时调试C#和C++代码,并设置断点。
  2. 使用DebugBreak()__debugbreak():在C++代码的关键位置插入这些函数调用,程序运行到此处会中断,方便查看调用栈和变量。
  3. 日志输出:在C++和C#两端都添加详细的日志,记录函数调用参数、返回值、内存地址等,这是定位跨语言问题最有效的手段之一。
  4. 使用Marshal.SizeOf():在C#中调用Marshal.SizeOf(typeof(MyStruct))来检查托管结构体的大小,确保与C++端的sizeof(MyStruct)一致。

6.4 部署注意事项

  1. VC++运行时库:如果你的C++ DLL是动态链接到VC++运行时的(/MD/MDd编译选项),目标机器必须安装对应版本的VC++可再发行组件包。建议将其作为安装程序的必备组件。
  2. DLL放置:对于私有DLL(只供你的应用使用),建议放在应用程序目录下。对于共享DLL,可以放在系统目录或通过安装程序注册,但要注意版本冲突(DLL Hell)。
  3. 清单文件:对于需要特定版本运行时的DLL,可以使用应用程序清单文件来指定依赖。

7. 一个综合案例:封装一个简单的图像处理DLL

假设我们有一个C++的图像处理库ImageProc.dll,它提供以下功能:

  1. 创建和销毁图像对象(返回句柄)。
  2. 从文件加载图像。
  3. 将图像转换为灰度图。
  4. 获取图像的宽度和高度。

我们将用P/Invoke来封装它,并演示资源管理和错误处理。

C++头文件 (ImageProc.h):

#pragma once #ifdef IMAGEPROC_EXPORTS #define IMAGEPROC_API __declspec(dllexport) #else #define IMAGEPROC_API __declspec(dllimport) #endif extern "C" { typedef void* ImageHandle; IMAGEPROC_API ImageHandle CreateImage(); IMAGEPROC_API void DestroyImage(ImageHandle handle); IMAGEPROC_API int LoadImageFromFile(ImageHandle handle, const char* filePath); IMAGEPROC_API int ConvertToGrayscale(ImageHandle handle); IMAGEPROC_API int GetImageWidth(ImageHandle handle, int* width); IMAGEPROC_API int GetImageHeight(ImageHandle handle, int* height); // 错误码:0=成功,负数=错误 }

C#封装类 (NativeImageProcessor):

using System.Runtime.InteropServices; using System.Text; namespace ImageInterop { // 使用SafeHandle封装非托管图像句柄 public sealed class SafeImageHandle : SafeHandleZeroOrMinusOneIsInvalid { private SafeImageHandle() : base(true) { } protected override bool ReleaseHandle() { if (!IsInvalid) { DestroyImage(handle); handle = IntPtr.Zero; } return true; } [DllImport("ImageProc.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] private static extern void DestroyImage(IntPtr handle); } public static class NativeImageProcessor { [DllImport("ImageProc.dll", CallingConvention = CallingConvention.Cdecl)] private static extern SafeImageHandle CreateImage(); [DllImport("ImageProc.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] private static extern int LoadImageFromFile(SafeImageHandle handle, string filePath); [DllImport("ImageProc.dll", CallingConvention = CallingConvention.Cdecl)] private static extern int ConvertToGrayscale(SafeImageHandle handle); [DllImport("ImageProc.dll", CallingConvention = CallingConvention.Cdecl)] private static extern int GetImageWidth(SafeImageHandle handle, out int width); [DllImport("ImageProc.dll", CallingConvention = CallingConvention.Cdecl)] private static extern int GetImageHeight(SafeImageHandle handle, out int height); // 封装为更友好的.NET API public static SafeImageHandle Create() { var handle = CreateImage(); if (handle.IsInvalid) { throw new InvalidOperationException("Failed to create image handle."); } return handle; } public static void LoadFromFile(SafeImageHandle handle, string filePath) { int result = LoadImageFromFile(handle, filePath); CheckResult(result, "Failed to load image from file."); } public static void ToGrayscale(SafeImageHandle handle) { int result = ConvertToGrayscale(handle); CheckResult(result, "Failed to convert image to grayscale."); } public static (int Width, int Height) GetDimensions(SafeImageHandle handle) { int width, height; int resultW = GetImageWidth(handle, out width); int resultH = GetImageHeight(handle, out height); if (resultW != 0) CheckResult(resultW, "Failed to get image width."); if (resultH != 0) CheckResult(resultH, "Failed to get image height."); return (width, height); } private static void CheckResult(int result, string message) { if (result != 0) // 假设0为成功 { throw new InvalidOperationException($"{message} Native error code: {result}"); } } } // 使用示例 class Program { static void Main() { try { using (var imageHandle = NativeImageProcessor.Create()) { NativeImageProcessor.LoadFromFile(imageHandle, @"C:\test.jpg"); var dimensions = NativeImageProcessor.GetDimensions(imageHandle); Console.WriteLine($"Image loaded: {dimensions.Width}x{dimensions.Height}"); NativeImageProcessor.ToGrayscale(imageHandle); Console.WriteLine("Image converted to grayscale."); // ... 其他操作 } // 离开using范围时,SafeImageHandle.Dispose()会自动调用ReleaseHandle销毁原生资源 } catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); } } } }

这个案例展示了如何结合SafeHandle进行资源管理、如何将C风格的错误码转换为.NET异常、以及如何构建一个更符合.NET使用习惯的封装层。在实际项目中,你可能还需要处理更复杂的数据交换(如图像像素数据的传递),其核心思想依然是精确控制内存布局和生命周期。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询