P/Invoke,C# 与 C++ 交互技术

P/Invoke『Platform Invoke』是可用于从托管代码访问非托管库中的结构、回调和函数的一种技术。

本博文教程源码 https://gitee.com/luli100/pinvoke-tutorial

结构体的封装和调用方式

默认情况下,非托管结构与托管结构在内存中的布局不同,因此,成功跨托管/非托管边界传递结构需要额外的步骤来保留数据完整性。

#include <iostream>
#include <comdef.h>
using namespace std;

struct Book
{
    int32_t id;
    BSTR name;
    float price;
};

extern "C"
{
    __declspec(dllexport) void Print(Book book)
    {
        cout << "Book Id: " << book.id << endl;
        wcout << "Book Name: " << book.name << endl;
        cout << "Book Price: " << book.price << endl;
    }

    __declspec(dllexport) Book Generate()
    {
        Book book = Book();
        book.id = 100;
        book.name = SysAllocString(L"C++");
        book.price = 100.0;
        return book;
    }
}

非托管模块是一个 DLL,它定义了一个 Book 结构和两个函数(Generate,Print)。

using System.Runtime.InteropServices;

namespace TestShoppingService
{
    [StructLayout(LayoutKind.Sequential)]
    internal struct Book
    {
        public Int32 Id;
        [MarshalAs(UnmanagedType.BStr)]
        public String Name;
        public Single Price;
    }

    class Program
    {
        [DllImport("ShoppingService.dll", EntryPoint = "Generate")]
        private static extern Book Generate();
        [DllImport("ShoppingService.dll", EntryPoint = "Print")]
        private static extern void Print(Book book);

        static void Main(String[] args)
        {
            var book = Generate();
            Print(book);
            book.Name = "C#";
            book.Price = 200;
            Print(book);

            Console.ReadLine();
        }
    }
}

托管模块,也即 C# 代码,它导入非托管 DLL 中的 Generate 函数和 Print 函数,根据托管模块重新定义了一个 Book 结构来等效于非托管模块的 Book 结构。事实上,这两个结构的名字可以不同,内容大小一致也可以完成同样的功能。

请注意,DLL 的任何部分都未使用传统的 #include 指令向托管代码公开。DLL 仅在运行时访问,因此在编译时不会检测到使用 DllImport 导入函数的问题。

为什么要使用 extern “C”

C++ 编译器会给程序中的每个函数换一个独一无二的名字。在 C 语言中,这个过程是不需要的,因为没有函数重载。重载不兼容于绝大部分链接程序,因为链接程序通常无法分辨同名的函数。名变换是对链接程序的妥协;链接程序通常坚持函数名必须独一无二。不要将 extern “C” 看作是声明这个函数是用 C 语言写的,应该看作是声明这个函数被当作好象 C 语言写的一样而进行调用。不管如何,它总意味着一件事:名变换被禁止了。

类的封装和调用方式

由于 C# 中结构是值类型,类是引用类型,因此,在封送 C++ 类时,需要做特殊处理。

#include <iostream>
using namespace std;

class Book
{
public:
    Book(int32_t id, string name, float price);
    void Print();
    void ChangeName(string name);
    void UpdatePrice(float price);
private:
    int32_t id;
    string name;
    float price;
};

Book::Book(int32_t id, string name, float price) :id(id), name(name), price(price)
{

}

void Book::Print()
{
    cout << "Book Id: " << id << endl;
    cout << "Book Name: " << name << endl;
    cout << "Book Price: " << price << endl;
}

void Book::ChangeName(string name)
{
    this->name = name;
}

void Book::UpdatePrice(float price)
{
    this->price = price;
}

extern "C"
{
    __declspec(dllexport) Book* CreateBook(int32_t id, const char* name, float price)
    {
        return new Book(id,name,price);
    }

    __declspec(dllexport) void DeleteBook(Book* book)
    {
        delete book;
    }

    __declspec(dllexport) void Print(Book* book)
    {
        book->Print();
    }

    __declspec(dllexport) void ChangeName(Book* book, const char* name)
    {
        book->ChangeName(name);
    }

    __declspec(dllexport) void UpdatePrice(Book* book, float price)
    {
        book->UpdatePrice(price);
    }
}

通常来说,C++ 代码会提供一个 C++ 原始类的包装类供 C# 调用。

using System.Runtime.InteropServices;

namespace TestShoppingService
{
    internal class Book
    {
        [DllImport("ShoppingService.dll", EntryPoint = "CreateBook")]
        private static extern IntPtr CreateBook(Int32 id, String name, Single price);
        [DllImport("ShoppingService.dll", EntryPoint = "DeleteBook")]
        private static extern void DeleteBook(IntPtr bookPtr);
        [DllImport("ShoppingService.dll", EntryPoint = "Print")]
        private static extern void Print(IntPtr bookPtr);
        [DllImport("ShoppingService.dll", EntryPoint = "ChangeName")]
        private static extern void ChangeName(IntPtr bookPtr, String name);
        [DllImport("ShoppingService.dll", EntryPoint = "UpdatePrice")]
        private static extern void UpdatePrice(IntPtr bookPtr, Single price);
        private IntPtr bookPtr;
        public Book(Int32 id, String name, Single price)
        {
            this.bookPtr = CreateBook(id,name,price);
        }

        ~Book()
        {
            DeleteBook(this.bookPtr);
            this.bookPtr = IntPtr.Zero;
        }

        public void Print()
        {
            Print(this.bookPtr);
        }

        public void ChangeName(String name)
        {
            ChangeName(this.bookPtr, name);
        }

        public void UpdatePrice(Single price)
        {
            UpdatePrice(this.bookPtr, price);
        }
    }

    class Program
    {
        static void Main(String[] args)
        {
            Book book = new Book(100,"C#",200);
            book.Print();
            book.ChangeName("Linux");
            book.UpdatePrice(300);
            book.Print();
            Console.ReadLine();
        }
    }
}

C# 通常也提供一个类来封装 C++ 的包装类,供其它 C# 代码调用。

C++ 如何调用 C# 函数

有时候,我们需要 C++ 可以主动调用 C# 函数。我们知道,C# 实现回调功能可以用委托进行包装,最为关键的是 C++ 可以通过函数指针直接接收 C# 委托来实现回调。

#include <iostream>

extern "C"
{
    typedef void(__stdcall* PCALLBACK) (int32_t count);
    PCALLBACK pcallback = nullptr;
    _declspec(dllexport) void SetCallback(PCALLBACK callback)
    {
        pcallback = callback;
    }

    _declspec(dllexport) void Notify()
    {
        if (pcallback != nullptr)
        {
            pcallback(100);
        }
    }
}

从 C++ 代码可以看到,PCALLBACK 就是定义的函数指针,接收一个 int32_t 参数。SetCallback 函数用于将 C# 传递进来的委托转换为函数指针。Notify 函数的作用是向 C# 提供一个函数调用,从而触发 C++ 代码调用 C# 委托。

class Program
{
    [UnmanagedFunctionPointer(CallingConvention.StdCall)]
    public delegate void DataChangedCallBack(Int32 value);
    [DllImport("ShoppingService.dll", EntryPoint = "SetCallback")]
    private static extern void SetCallback(DataChangedCallBack callback);
    [DllImport("ShoppingService.dll", EntryPoint = "Notify")]
    private static extern void Notify();
    static void Main(String[] args)
    {
        SetCallback((t) => { Console.WriteLine($"C++ 调用 C# 函数,结果为:{t}"); });
        Notify();
        Console.ReadLine();
    }
}

需要注意的是,DataChangedCallBack 委托特性标注 [UnmanagedFunctionPointer(CallingConvention.StdCall)] 中的 CallingConvention.StdCall 应与 C++ 代码的函数指针声明 __stdcall 保持一致。

结论

当你遇到 C++ 代码比 C# 代码更能解决你的需求场景时,千万别忘了 P/Invoke 的存在。