平台调用P/Invoke介绍

作者:Chdon 发布时间: 2025-12-25 阅读量:3 评论数:0

平台调用P/Invoke介绍

前提知识

一、手动监视和控制对象的生命周期:GCHandle

GCHandle主要用于提供从非托管内存访问托管对象的方式。通过其可以创建一个句柄,能用对应任意托管对象。

GCHandle分为四种类型:Weak, WeakTrackResurrection, Normal, Pinned

  • GCHandleType.Weak: 创建一个普通的弱引用,指向目标对象。

    通过调用handle.Target == null来判断对象是否被回收

    var obj = new MyClass();
    GCHandle handle = GCHandle.Alloc(obj, GCHandleType.Weak);
    // 当 obj 不再有强引用时:
    GC.Collect();
    
    // 检查弱引用是否存活
    if (handle.Target == null)
    {
    Console.WriteLine("对象已被回收");
    }
    
  • GCHandleType.WeakTrackResurrection:创建一个弱引用,并跟踪对象是否被复活

    若对象在终结阶段被复活(例如通过 GC.ReRegisterForFinalize 或其他方式),WeakTrackResurrection 的句柄仍然可以访问复活后的对象。

    public class ResurrectableClass
    {
    ~ResurrectableClass()
    {
      // 在终结器中复活对象
      SomeGlobal.RevivedInstance = this;
      GC.ReRegisterForFinalize(this);
    }
    }
    
    var obj = new ResurrectableClass();
    GCHandle handle = GCHandle.Alloc(obj, GCHandleType.WeakTrackResurrection);
    
    // 当 obj 被 GC 回收并复活后:
    GC.Collect();
    
    // 检查弱引用是否存活(复活后仍可访问)
    if (handle.Target != null)
    {
    Console.WriteLine("对象已复活");
    }
    
  • GCHandleType.Normal: 对象不会被垃圾回收,需要手动调用Free进行释放。

    主要用于托管代码与非托管代码交互时,安全的传递上下文

    using System;
    using System.Runtime.InteropServices;
    
    public class DataProcessor
    {
    public void LogMessage(string message)
    {
      Console.WriteLine($"日志: {message}");
    }
    }
    
    public class Program
    {
    // 定义非托管回调委托
    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate void CallbackDelegate(string message, IntPtr context);
    
    // 非托管函数声明
    [DllImport("NativeLib.dll")]
    public static extern void RegisterLogger(CallbackDelegate callback, IntPtr context);
    
    [DllImport("NativeLib.dll")]
    public static extern void TriggerLogging(string message);
    
    static void Main()
    {
      DataProcessor logger = new DataProcessor();
      GCHandle handle = GCHandle.Alloc(logger, GCHandleType.Normal);
      IntPtr context = GCHandle.ToIntPtr(handle);
    
      // 注册回调和上下文
      CallbackDelegate callback = (msg, ctx) =>
      {
          GCHandle h = GCHandle.FromIntPtr(ctx);
          if (h.Target is DataProcessor processor)
          {
              processor.LogMessage(msg);
          }
      };
      RegisterLogger(callback, context);
    
      // 触发非托管代码记录日志
      TriggerLogging("测试非托管回调");
    
      // 清理
      handle.Free();
    }
    }
    
    #include <iostream>
    #include <string>
    
    typedef void (*CallbackDelegate)(const char* message, void* context);
    
    static CallbackDelegate g_callback = nullptr;
    static void* g_context = nullptr;
    
    extern "C" __declspec(dllexport)
    void RegisterLogger(CallbackDelegate callback, void* context) {
    g_callback = callback;
    g_context = context;
    }
    
    extern "C" __declspec(dllexport)
    void TriggerLogging(const char* message) {
    if (g_callback != nullptr) {
      g_callback(message, g_context); // 触发回调,传回上下文
    }
    }
    
  • GCHandleType.Pinned: 对象不会被垃圾回收,并且不会被垃圾回收器移动。

    • 可通过调用handle.AddrOfPinnedObject()获取对象实际内存地址,未被PinnedGCHandle,调用该函数时,会报错。

二、平台特定的整数类型:IntPtr

IntPtr用于表示本机资源,主要存储句柄或内存地址,两者不可混用。

其大小取决于操作系统是32bit还是64bit,等于当前系统指针占用大小。

句柄或内存地址,两者不可混用

int[] intArr = new int[] { 1, 2, 3 };
// ❌错误,此处获取的是句柄,无法直接转换为对象内存地址使用
// var handle = GCHandle.Alloc(intArr);
// IntPtr arrIntPtr = GCHandle.ToIntPtr(handle);

// ✔正确,需要通过Pinned对象,以获取其内存地址
var handle = GCHandle.Alloc(intArr, GCHandleType.Pinned);
IntPtr arrIntPtr = handle.AddrOfPinnedObject();

unsafe
{
    int* ptr = (int*)arrIntPtr;
    Console.WriteLine(ptr[0]);
}
Console.ReadLine();

// ❌错误输出:45425712(或者超出内存范围,抛出`System.AccessViolationException`)
// ✔正确输出: 1

作为句柄的情况:

  1. 窗口句柄(HWND)

    • 用途:表示窗口的标识符,用于操作窗口(如最小化、关闭、发送消息等)。
    • 示例
    // 获取记事本窗口句柄
    IntPtr hWnd = FindWindow("Notepad", null);
    // 发送关闭消息
    SendMessage(hWnd, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
    

  1. 设备上下文句柄(HDC)

    • 用途:表示图形绘制上下文,用于绘图操作(如GDI绘图)。

    • 示例

      IntPtr hdc = GetDC(hWnd);  // 获取窗口的设备上下文
      Rectangle(hdc, 10, 10, 100, 100);  // 绘制矩形
      ReleaseDC(hWnd, hdc);  // 释放句柄
      

  1. 文件或资源句柄(HANDLE)

    • 用途:表示打开的文件、管道、设备等资源。

    • 示例

      // 打开文件(返回句柄)
      IntPtr hFile = CreateFile("test.txt", GENERIC_READ, 0, IntPtr.Zero, OPEN_EXISTING, 0, IntPtr.Zero);
      // 读取文件内容
      byte[] buffer = new byte[1024];
      ReadFile(hFile, buffer, buffer.Length, out int bytesRead, IntPtr.Zero);
      CloseHandle(hFile);  // 必须手动关闭句柄
      

  1. 模块/库句柄(HMODULE/HINSTANCE)

    • 用途:表示加载的DLL或可执行模块的内存基址。

    • 示例

      // 加载DLL
      IntPtr hModule = LoadLibrary("user32.dll");
      // 获取函数地址
      IntPtr funcPtr = GetProcAddress(hModule, "MessageBoxW");
      FreeLibrary(hModule);  // 卸载库
      

  1. GDI对象句柄(HBITMAP/HBRUSH/HPEN等)

    • 用途:表示图形设备接口对象(如位图、画刷、画笔)。

    • 示例

      // 创建红色画刷
      IntPtr hBrush = CreateSolidBrush(0x0000FF);
      // 选入设备上下文
      SelectObject(hdc, hBrush);
      DeleteObject(hBrush);  // 必须删除对象
      

  1. 进程/线程句柄

    • 用途:用于操作进程或线程(如终止、查询状态)。

    • 示例

      // 打开进程句柄
      IntPtr hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, processId);
      TerminateProcess(hProcess, 0);  // 终止进程
      CloseHandle(hProcess);
      

  1. 内核对象句柄(HANDLE)

    • 用途:表示互斥体(Mutex)、事件(Event)、信号量(Semaphore)等同步对象。

    • 示例

      // 创建互斥体
      IntPtr hMutex = CreateMutex(IntPtr.Zero, false, "MyMutex");
      WaitForSingleObject(hMutex, INFINITE);  // 等待获取所有权
      ReleaseMutex(hMutex);
      CloseHandle(hMutex);
      

  1. 图标/光标句柄(HICON/HCURSOR)

    • 用途:操作图标或光标资源。

    • 示例

      // 加载系统图标
      IntPtr hIcon = LoadIcon(IntPtr.Zero, IDI_APPLICATION);
      DestroyIcon(hIcon);  // 销毁图标
      

  1. 注册表键句柄(HKEY)

    • 用途:表示打开的注册表键。

    • 示例

      IntPtr hKey;
      RegOpenKeyEx(HKEY_CURRENT_USER, "Software\\MyApp", 0, KEY_READ, out hKey);
      // 读取注册表值...
      RegCloseKey(hKey);
      

  1. 套接字句柄(SOCKET)

    • 用途:表示网络通信的套接字(在Windows中定义为SOCKET,实际类型为IntPtr)。

    • 示例

      SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
      closesocket(s);  // 关闭套接字
      

作为内存地址的情况:

  1. 分配/释放非托管内存

    • 场景:手动分配非托管内存块供非托管代码使用。

    • 示例

      // 声明内存分配/释放API
      [DllImport("kernel32.dll")]
      private static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
      
      [DllImport("kernel32.dll")]
      private static extern bool VirtualFree(IntPtr lpAddress, uint dwSize, uint dwFreeType);
      
      // 使用示例
      IntPtr buffer = VirtualAlloc(IntPtr.Zero, 4096, 0x1000 /*MEM_COMMIT*/, 0x04 /*PAGE_READWRITE*/);
      if (buffer != IntPtr.Zero) 
      {
          // 传递给非托管函数
          NonManagedProcessBuffer(buffer, 4096);
          VirtualFree(buffer, 0, 0x8000 /*MEM_RELEASE*/); // 必须释放
      }
      

  1. 托管数组与非托管内存互拷

    • 场景:在托管数组和非托管内存间复制数据。

    • 示例

      // 分配非托管内存
      IntPtr unmanagedArray = Marshal.AllocHGlobal(1024);
      
      // 托管数组 -> 非托管内存
      byte[] managedData = new byte[1024];
      Marshal.Copy(managedData, 0, unmanagedArray, managedData.Length);
      
      // 非托管内存 -> 托管数组
      byte[] resultData = new byte[1024];
      Marshal.Copy(unmanagedArray, resultData, 0, resultData.Length);
      
      Marshal.FreeHGlobal(unmanagedArray); // 释放
      

  1. 传递结构体内存地址

    • 场景:将结构体序列化到非托管内存,供非托管代码解析。

    • 示例

      [StructLayout(LayoutKind.Sequential)]
      public struct Point 
      {
          public int X;
          public int Y;
      }
      
      // 声明接受结构体指针的非托管函数
      [DllImport("NativeLib.dll")]
      private static extern void ProcessPoint(IntPtr pPoint);
      
      // 使用示例
      Point point = new Point { X = 10, Y = 20 };
      IntPtr ptr = Marshal.AllocHGlobal(Marshal.SizeOf<Point>());
      Marshal.StructureToPtr(point, ptr, false);
      ProcessPoint(ptr); // 非托管代码直接读取ptr指向的内存
      Marshal.DestroyStructure<Point>(ptr);
      Marshal.FreeHGlobal(ptr);
      

  1. 固定托管对象内存

    • 场景:防止GC移动托管数组内存,直接传递地址。

    • 示例

      byte[] managedBuffer = new byte[1024];
      GCHandle handle = GCHandle.Alloc(managedBuffer, GCHandleType.Pinned);
      try 
      {
          IntPtr pinnedAddress = handle.AddrOfPinnedObject();
          NonManagedProcessData(pinnedAddress, managedBuffer.Length); // 安全传递地址
      }
      finally 
      {
          handle.Free(); // 必须解除固定
      }
      

  1. 直接操作内存(unsafe代码)

    • 场景:通过指针直接读写内存。

    • 示例

      unsafe 
      {
          int[] numbers = { 1, 2, 3 };
          fixed (int* p = numbers) 
          {
              IntPtr ptr = (IntPtr)p;
              NonManagedSumArray(ptr, numbers.Length); // 非托管函数求和
          }
      }
      

  1. 接收非托管代码返回的堆内存

    • 场景:非托管函数通过malloc分配内存,C#负责释放。

    • 示例

      [DllImport("NativeLib.dll")]
      private static extern IntPtr GenerateData(int size); // 返回malloc分配的内存
      
      [DllImport("NativeLib.dll")]
      private static extern void FreeData(IntPtr ptr); // 调用free释放
      
      // 使用示例
      IntPtr dataPtr = GenerateData(1024);
      byte[] managedData = new byte[1024];
      Marshal.Copy(dataPtr, managedData, 0, 1024);
      FreeData(dataPtr); // 必须调用对应的释放函数
      

  1. 处理图像像素数据

    • 场景:操作位图的原始像素内存。

    • 示例

      Bitmap bitmap = new Bitmap(800, 600);
      BitmapData data = bitmap.LockBits(new Rectangle(0, 0, 800, 600), 
                                       ImageLockMode.ReadWrite, 
                                       PixelFormat.Format32bppArgb);
      try 
      {
          IntPtr scan0 = data.Scan0; // 像素内存起始地址
          NonManagedProcessPixels(scan0, data.Stride, data.Height); // 非托管图像处理
      }
      finally 
      {
          bitmap.UnlockBits(data); // 解锁内存
      }
      

  1. 与COM对象交换指针

    • 场景:传递COM接口指针的内存地址。

    • 示例

      IMyComObject comObj = new MyComObject();
      IntPtr comPtr = Marshal.GetComInterfaceForObject(comObj, typeof(IMyComObject));
      try 
      {
          NonManagedUseComInterface(comPtr); // 非托管代码通过指针调用COM方法
      }
      finally 
      {
          Marshal.Release(comPtr); // 减少引用计数
      }
      

  1. 跨进程内存访问

    • 场景:读写其他进程的内存地址。

    • 示例

      [DllImport("kernel32.dll")]
      private static extern bool ReadProcessMemory(
          IntPtr hProcess, 
          IntPtr lpBaseAddress,
          byte[] lpBuffer, 
          int nSize, 
          out int lpNumberOfBytesRead
      );
      
      // 使用示例
      Process process = Process.GetProcessById(1234);
      IntPtr processHandle = OpenProcess(ProcessAccessFlags.VMRead, false, process.Id);
      byte[] buffer = new byte[4];
      ReadProcessMemory(processHandle, new IntPtr(0x00400000), buffer, 4, out _);
      CloseHandle(processHandle);
      

  1. 自定义内存池管理

    • 场景:高性能场景中重用非托管内存。

    • 示例

      public class MemoryPool : IDisposable
      {
          private IntPtr _buffer;
          private const int BlockSize = 4096;
      
          public MemoryPool() 
          {
              _buffer = Marshal.AllocHGlobal(BlockSize * 100); // 预分配100个块
          }
      
          public IntPtr AllocateBlock() 
          {
              // 返回下一个可用块的地址(简化逻辑)
              return _buffer + _currentIndex++ * BlockSize;
          }
      
          public void Dispose() 
          {
              Marshal.FreeHGlobal(_buffer);
          }
      }
      

如何调用一个C++导出的DLL?

在.NET程序中,P/Invoke是调用C/C++函数的一种方式。

简单示例程序

首先,创建一个本地方法print_line

添加文件NativeLib.h:

#pragma once

// 使用 extern "C" 禁止名称修饰
#ifdef __cplusplus
extern "C" {
#endif

// 明确调用约定(如__stdcall或__cdecl)
#ifndef MYAPI
#define MYAPI __declspec(dllexport)
#endif

	MYAPI void __cdecl print_line(const char* str);

#ifdef __cplusplus
}
#endif

然后,再添加NativeLib.cpp:

#include "NativeLib.h"
#include <stdio.h>

MYAPI void print_line(const char* str) {
  printf("%s\n", str);
}

现在添加C#调用代码Program.cs

using System.Runtime.InteropServices;

namespace PInvokeTest_1_Basic
{
    internal class Program
    {
        static void Main(string[] args)
        {
            print_line("Hello, PInvoke!");
        }

        [DllImport("NativeLibDll.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
        private static extern void print_line(string str);
    }
}

在这里,我们指定了需要导入进.NET类的C/C++函数。有以下几点需要关注的:

  • 修饰符是static externextern代表这个函数是从C/C++导入的;static是必要的,因为这个函数对类Program一无所知。
  • 函数的名称需要与C/C++的函数名称保持一致。
  • 参数str是一个.NET类型(string)。P/Invoke自动将数据类型从.NET类型转换到C/C++类型。
  • attribute[DllImport]指定了能从中导入函数的DLL文件名。需要关注的是,DllImport使得你几乎能够控制导入的每个方面,例如指定一个不同的.NET方法名或者指定调用约定(calling convention)。
  • Ansi不同地区的系统中代表不同的编码。中文Windows使用GBK, 英文字符占用一个字节,非英文字符占用两个字节。

问题解决

1. 未能够加载DLL

你可能会看到一个异常DllNotFoundException,并带有类似“找不到指定模块。”的提示消息。

Snipaste_2025-12-05_09-07-03.bmp
正如错误信息所示,DLL“NativeLib.dll”未能找到。
只需要将“NativeLib.dll”文件,复制到当前可执行文件运行目录即可。

2. 栈不平衡

你可能会遇到一个报错,PInvokeStackImbalance was detected

stack-imbalance-1764924792505-4.png

产生这种报错大概率是因为本地库使用另一种调用约定(calling convention)。默认情况下,C/C++使用__cdecl的调用约定,而[DllImport]默认使用的是__stdcall的调用约定。

解决方案:保证调用约定的一致性:

  • [DllImport]中指定正确的调用约定,例如[DllImport("NativeLib.dll", CallingConvention=CallingConvention.Cdecl)]
  • 在本地项目中修改默认的调用约定,可以在项目设置中C/C++ –> Advanced –> Calling Convention
  • 在C/C++函数前添加期望的调用约定,例如void __stdcall print_line(const char* str)。这将只会改变这些函数的调用约定。

3. 找不到入口点

你可能会遇到一个报错,找不到名为“print_line”的入口点。
Snipaste_2025-12-05_09-12-03.bmp

解决方案:

  • 检查调用函数名称是否一致,可在[DllImport]中指定函数入口点,例如[DllImport("NativeLibDll.dll", ..., EntryPoint = "print_line")];或者,直接在extern中保持函数名与函数入口点一致。
  • 函数名可能在编译时被C++修饰,需要使用extern "C"进行函数的导出。

其他类型的数据类型如何封送?

P/Invoke通过自动转化("marshalling")从托管代码到本机代码的数据类型,使得调用更加简单。反之亦然。

封送原始数据类型

原始数据类型(bool,int,double,...)是最简单易用的,它们会被直接映射到相对应的类型。

C# typeC/C++ typeBytesRange
boolbool (with int fallback)usually 1true or false
charwchar_t (or char if necessary)2 (1)Unicode BMP
byteunsigned char10 to 255
sbytechar1-128 to 127
shortshort2-32,768 to 32,767
ushortunsigned short20 to 65,535
intint4-2 billion to 2 billion
uintunsigned int40 to 4 billion
long__int648-9 quintillion to 9 quintillion
ulongunsigned __int6480 to 18 quintillion
floatfloat47 significant decimal digits
doubledouble815 significant decimal digits

封送字符串

若需传递字符串,建议你将它们以Unicode(UTF-16)的编码形式进行传递(如果可能的话)。你需要像这样指定Char.Unicode

[DllImport("NativeLib.dll", CharSet = CharSet.Unicode)]
private static extern void do_something(string str);

这需要C/C++的参数类型是wchar_t*

void do_something(const wchar_t* str);

封送数组

原始类型的数组能够直接传递。

[DllImport("NativeLib.dll")]
private static extern void do_something(byte[] data);

封送对象

若需传递对象,必须将其内存布局声明为顺序布局(Sequential):

[StructLayout(LayoutKind.Sequential)]
class MyClass {
  ...
}

这个保证了字段能够按照编写的顺序被存储。(没有这个Attribute,C#编译器会重新排列字段来优化数据结构)

然后直接使用这个对象的类型:

[DllImport("NativeLib.dll")]
private static extern void do_something(MyClass data);

这个对象会被按引用传递(struct*/struct&)给C的函数:

typedef struct {
  ...
} MyClass;

void do_something(MyClass* data);

本机Struct的字段顺序需要跟托管对象类型字段顺序保持一致。

封送结构

封送托管struct几乎和封送对象是一致的,唯一的不同是struct默认是按照复制传递。
所以对于struct,C/C++方法的前面是这样的:

void do_something(MyClass data);

当然,你也可以按照引用传递struct。在这种情况下,在C/C++中使用(MyClass* data)(MyClass& data),并且在C#中使用(ref MyClass data)

封送委托

委托是被直接封送的。唯一需要关注的是注意调用约定(calling convention)。默认的调用约定是Winapi(在Window中等同于StdCall)。如果你的本地库使用了不同的调用约定,你需要像这样指定:

// C#
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void MyDelegate(IntPtr value);
typedef void(__cdecl* MyDelegate)(void* value);

封送任意指针

任意指针(例如void *)会被按照IntPtr封送.

// C++
void do_something(void* ptr);
// C#
[DllImport("NativeLib.dll")]
private static extern void do_something(IntPtr ptr);

如何保证垃圾回收时,对象的地址不发生改变?

有时,一个C/C++函数需要保存你传递的数据,后续进行引用处理。如果这样的数据是一个托管对象(例如stringclass), 你需要确保在本机代码仍然在使用或者存储的时候,它们没有被垃圾回收器回收。

这就是为什么要固定。它使得垃圾回收器不能删除或者移动该对象。

固定一个对象

我们可以使用GCHandle.Alloc,来固定一个托管对象:

// Pin "objectToBePinned"
GCHandle handle = GCHandle.Alloc(objectToBePinned, GCHandleType.Pinned);

objectToBePinned会保持固定状态,直到你调用Free()

// Unpin "objectToBePinned"
handle.Free();

在这个对象被取消固定后,它可以再一次:

  • 被垃圾回收器移动(为了优化内存结构)。
  • 被删除,如果它没有被引用。

注意:

  • Free()不会自动调用。如果你没有手动调用它,被固定对象的内存将永远不会被释放(导致内存泄漏)。
  • 你只需要固定对象(包括字符串)。你不能固定原始类型(例如int)和结构(struct),因为它们保存在栈上,并且通过复制传递。尝试固定struct,只会固定struct的拷贝副本。
  • 类和结构必须要带有Attribute:[StructLayout(LayoutKind.Sequential)],来控制字段的内存布局。否则调用GCHandle.Alloc()将会抛出一个异常ArgumentException: “Object contains non-primitive or non-blittable data”。
  • 如果你调用的方法不会存储传递的对象,来后续使用,那么你不需要固定对象。P/Invoke会在调用C/C++函数之前自动固定对象,并且在函数返回之后取消固定。所以手动固定对象的关键,实际上在于(何时)解除固定。

传递一个固定对象

固定对象后,通常需要将其传递给 C/C++ 函数。最简单的方式是在 P/Invoke 方法中直接指定托管类型:

// 直接使用 "MyType" 作为参数类型
[DllImport("NativeLib")]
private static extern void do_something(MyType myType);

然后调用这个方法:

GCHandle handle = GCHandle.Alloc(objectToBePinned, GCHandleType.Pinned);

do_something(objectToBePinned);
...
handle.Free();

另外一种方法是通过IntPtr传递(尽管和直接传递没有区别):

[DllImport("NativeLib")]
private static extern void do_something(IntPtr myType);

...

GCHandle handle = GCHandle.Alloc(objectToBePinned, GCHandleType.Pinned);

do_something(handle.AddrOfPinnedObject());

...
handle.Free();

固定并传递字符串

固定字符串和固定对象是一样的,需要注意的是:
在传递固定字符串是,你必须指定CharSet.Unicode
否则 P/Invoke 会将字符串转换为 ASCII 格式(导致额外的内存拷贝)。
假设有以下 C 函数:

void do_something(void* str1, void* str2) {
  printf("Equals: %s\n", (str1 == str2 ? "true" : "false"));
}

然后C#调用:

// ❌,将会输出 "false"
[DllImport("NativeLib", EntryPoint="do_something")]
private static extern void do_something1(string str1, IntPtr str2);

// ✔,将会输出 "true"
[DllImport("NativeLib", CharSet = CharSet.Unicode, EntryPoint="do_something")]
private static extern void do_something2(string str1, IntPtr str2);

...

string text = "my text";
GCHandle handle = GCHandle.Alloc(text, GCHandleType.Pinned);

// ❌,将会输出 "false"
do_something1(text, handle.AddrOfPinnedObject());
// ✔,将会输出 "true"
do_something2(text, handle.AddrOfPinnedObject());

验证固定对象是否被正确传递

正如前文所述,P/Invoke 可能会隐式复制对象,而非直接传递引用。
要确认是否发生复制,可以比较指针地址:在C#中可以调用handle.AddrOfPinnedObject().ToString()来获取被固定对象的地址。

评论