本文将以C#为例,讲解SWIG的原理
原文链接:https://www.yuque.com/cppdev/swig/nuaxps
SWIG本质上是一个代码生成器,它帮我们生成了一个中间层,使得目标语言能够调用到C/C++的代码。
接下来,我们将自己动手编写这个中间层,看看SWIG到底为我们做了什么。
文章目录
代码框架
本文代码:https://github.com/geodoer/swig-examples/tree/main/A-HowSWIGWork
本示例代码的框架如下:
simple
模块是我们写的C/C++代码usesimple
模块是C#客户端程序,需要调用我们的C/C++代码simple_csharp
是C#中间层,它的职责有两个- 为C#客户端提供和C++代码一样的接口(如
simple.cs
) - 通过动态链接库,调用C/C++的代码,完成整个流程(如
simplePINVOKE.cs
文件)
- 为C#客户端提供和C++代码一样的接口(如
simple_wrap
是C++中间层,它将C/C++包装成C#想要的样子,供C#调用
simple模块(C/C++代码)
先来看一下C/C++代码。
C/C++代码很简单,只有一个example.h
文件,里面只有一个变量和一个函数。
而我们的目标就是在C#程序中,能够使用到这个全局变量和函数。
//File: example.h
/* A global variable */
SIMPLE_API extern double Foo;
/* Compute the greatest common divisor of positive integers */
SIMPLE_API extern int gcd(int x, int y);
usesimple(C#如何使用C/C++代码)
了解C/C++代码之后,我们再看看在C#中,会如何使用我们写的C++代码。
//File: Program.cs
using System;
namespace usesimple
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
//Program.cs
//调用C/C++的gcd()函数
int x = 42;
int y = 105;
int g = simple.gcd(x, y);
Console.WriteLine("The gcd of " + x + " and " + y + " is " + g);
//操纵C/C++的Foo全局变量
Console.WriteLine("Foo = " + simple.Foo); //输出当前值
simple.Foo = 3.1415926; //改变值
Console.WriteLine("Foo = " + simple.Foo); //看看改变是否生效
Console.ReadLine();
}
}
}
中间层
simple_csharp/simple.cs(提供给C#的接口文件)
在上一节中,我们明白了C#要如何使用C/C++的代码。
因此,我们很清楚的知道,我们需要提供给C#怎么样的接口。
//File: simple.cs
/* 提供给C#客户端调用的接口 */
public class simple
{
public static double Foo
{
set
{
//TODO:将value设置给example.h中的Foo变量
}
get
{
//TODO:获取example.h中Foo的值
}
}
public static int gcd(int x, int y)
{
//TODO:调用example.h中的gcd函数
}
}
simple_wrap模块(将C/C++代码进行包装)
根据simple.cs
文件,我们知道了要如何包装我们的C++代码了。
simple_wrap
即是一个中间层,它将simple
模块的C/C++代码,组织成C#需要的样子
//File:example_wrap.cpp
__declspec(dllexport) void __stdcall CSharp_Foo_set(double jarg1) {
Foo = jarg1;
}
__declspec(dllexport) double __stdcall CSharp_Foo_get() {
return Foo;
}
__declspec(dllexport) int __stdcall CSharp_gcd(int jarg1, int jarg2) {
return gcd(jarg1, jarg2);
}
simple_csharp/simplePINVOKE.cs(C#调用C++)
simple_csharp
模块中的simplePINIVOKE.cs
将通过动态链接库,调用在simple_wrap
模块中已经包装好C++函数,从而将整个流程打通,实现C#调用C++的通路。
//simplePINVOKE.cs
/* C#中间层
1. 调用C++中间层的代码,完成C#与C++之间的通讯
*/
class simplePINVOKE
{
static simplePINVOKE()
{
}
/// 从DLL中导入一个外部函数
///
/// DllImport的参数说明
/// 1. 第一个参数 simple_wrap DLL的名称
/// 2. 第二个参数 CSharp_Foo_set 入口点,即函数名称
[global::System.Runtime.InteropServices.DllImport("simple_wrap", EntryPoint = "CSharp_Foo_set")]
public static extern void Foo_set(double jarg1);
[global::System.Runtime.InteropServices.DllImport("simple_wrap", EntryPoint = "CSharp_Foo_get")]
public static extern double Foo_get();
/// 对于simple_csharp模块中的函数 int CSharp_gcd(int jarg1, int jarg2)
/// 映射到C#中,函数为签名为 int gcd(int jarg1, int jarg2)
///
/// 不难发现,这里其实做了一个映射
/// 1. C++的int类型 => C#的int类型
/// 2. C++的函数名称为CSharp_gcd => C#的函数名称为gcd
///
///
[global::System.Runtime.InteropServices.DllImport("simple_wrap", EntryPoint = "CSharp_gcd")]
public static extern int gcd(int jarg1, int jarg2);
}
这里使用了C#里的一个功能,global::System.Runtime.InteropServices.DllImport
它的作用是从动态链接库中导入函数
- 第一个参数,是动态链接库的名称
- 第二个参数,是入口点名称,在这里就是函数的名称
不难发现,在DllImport
中,做了一个函数映射
- C++的int类型 => C#的int类型
- C++的函数名称为CSharp_gcd => C#的函数名称为gcd
总结
simple_wrap、simple_csharp是一个中间层。
在实际应用中,example_wrap.cpp、simplePINVOKE.cs、simple.cs由SWIG帮我们自动生成。
而我们只需要拿到这些文件,组织成simple_wrap
、simple_csharp
模块即可。
请注意,模块划分不一定要像本文一样,这只是笔者建议的工程化模式。
后语
- 在
simple_csharp/simplePINVOKE.cs
中,只涉及了基础类型的映射。但在实际工程中,类型映射很复杂,有C++模板类、智能指针、C++STL、自定义类等等,这些该如何映射呢? - 还有对异常的处理,C++的异常要能抛到C#中。
- …
其实还有很多内容,都挺复杂的。
但SWIG都能帮我们处理,而且它也提供了灵活度很高的特性,使得我们能够自定义一些规则,完成工程化的需求。