25 C# 第二十章 平台互操作性和不安全的代码

概述


C#虽然功能强大,但在某些时刻必须也要对内存和指针进行操作。 C#中一共有三种方式:
1)  通过平台调用(P/Invoke)来调用非托管DLL所公开的API。
2)  通过不安全的代码,它允许我们访问内存地址和指针。
3)  通过COMInterop(COM 互操作),但此处不进行描述。


平台调用

1)  外部函数的声明

确定了要调用的目标函数后,P/Invoke的第一步便是用托管代码声明函数。
和一个类的普通方法一样,必须在一个类的上下文中声明目标API,但要添加 extern修饰符。


class PlatformInvokeTest
{
    [DllImport("msvcrt.dll")]
    public static extern int puts(string c);
    [DllImport("msvcrt.dll")]
    internal static extern int _flushall();
}


2)  参数的数据类型

定义了函数声明后,下一步便是标识或创建与外部函数中的非托管数据类型对应的托管数据类型。


例如:外部函数的定义为
LPVOID VirtualAllocEx(HANDLE  hProcess,
                      LPVOID  lpAddress,
                      SIZE_T  dwSize,
                      DWORD   flAllocationType,
                      DWORD   flProtect);


class PlatformInvokeTest
{
    [DllImport("kernel32.dll", SetLastError = true)]
    internal static extern IntPtr VirtualAllocEx(IntPtr  hProcess,
                                                 IntPtr  lpAddress,
                                                 IntPtr  dwSize,
                                                 AllocationType   flAllocationType,
                                                 uint    flProtect)


}


这里定义的托管函数与非托管函数的参数是一一对应的。


关于 IntPtr

托管代码的特征是像int这样的基本数据类型不会因处理器而改变大小。但在非托管代码中,内存指针会随处理器而变化。
因此不能将HANDLE 或 LPVOID映射到 int型上,而应把他们映射到System.IntPtr上,其大小将依据处理器的内存布局而变化。



3)  使用 ref 而不是指针

很多情况下,非托管代码会为传引用参数使用指针。在这些情况下,P/Invoke不要求你在托管代码中将数据类型映射到一个指针。应该将参数映射为 ref。


例如:
class PlatformInvokeTest
{
    [DllImport("kernel32.dll", SetLastError = true)]
    internal static extern IntPtr VirtualAllocEx(IntPtr  hProcess,
                                                 IntPtr  lpAddress,
                                                 IntPtr  dwSize,
                                                 AllocationType   flAllocationType,
                                                 uint    flProtect
                                                 ref   unit  lpflOldProtect);
}



4)  错误处理

Win32 API 编程的一个不便之处在于,错误报告的方式不一致,有时使用返回值,有时以参数输出的方式,有时使用GetLastError()函数。非托管代码中Win32的错误报告很少通过异常来生成。


P/Invoke 为此提供了相应的处理方式。如果需要启用这一方式,DLLImport 特性的SetLastError 参数要设为 true。这样就可以实例化 System.ComponentModel.Win32Exception()。在P/Invoke调用后,会自动用Win32数据来初始化它。


例如:

class VirtualMemoryManager
{
    [DLLImport("kernel32.dll", SetLastError = true)]
    private static extern IntPtr VirtualAllocEx(IntPtr  hProcess,
                                                IntPtr  lpAddress,
                                                IntPtr  dwSize,
                                                AllocationType flAllocationType,
                                                uint  flProtect);




    private static IntPtr AllocExecuteionBlock(int size, IntPtr  hProcess)
    {
        IntPtr  codeBytesPtr;
        codeBytesPtr = VirtualAllocEx(hProcess, IntPtr.Zero, (IntPtr)size, 
                                      AllocationType.Reserve,
                                      (uint)ProtectionOptions.PageExecuteReadWrite);




        if (codeBytesPtr == IntPtr.Zero)
        {
            throw new System.ComponentModel.Win32Exception();
        }
        return codeBytesPtr;
    }
}




5)  使用SafeHandle
很多时候P/Invoke会涉及资源清理和释放。

这时需要定义一个类派生自 System.Runtime.InteropServices.SafeHandle并且实现其中的ReleaseHandle() 方法, 在其中释放相关的资源。




指针和地址 (不安全代码)


有些情况下开发人员希望直接访问和操作内存,以及直接使用指针来定位内存。如果我们希望直接操纵内存,我们只需将一个代码区域指定为 unsafe(不安全)即可。


使用 unsafe 指定一个方法
class Program
{
    unsafe  static int Main()
    {
        // ...
    }
}


使用 unsafe 指定一个代码块
class Program
{
    static int Main()
    {
        unsafe
        {
            // ...
        }
    }
}



指针的声明和编写不安全的代码


代码实例:

using System;

namespace unsafe_test1
{
    class UnsafeTest
    {
        // Unsafe method: takes pointer to int: 
        unsafe static void SquarePtrParam(int* p)
        {
            *p *= *p;
        }


        unsafe static void Main()
        {
            int i = 5;
            // Unsafe method: uses address-of operator (&):
            SquarePtrParam(&i);
            Console.WriteLine(i);


            Console.ReadKey();
        }
    }
}


一些限制:

指针不能指向托管类型,例如 string, 类等。


为了将一些数据的地址赋给一个指针,要求如下:
-  数据必须属于一变量
-  数据必须是非托管类型
-  变量需要用 fixed 固定,不能移动


byte[] bytes = new byte[24];
fixed (byte* pData = & bytes[0])
{
    // ...
}



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值