目录
Swig主要是为了将c++/c中的代码所实现的功能移植到别的语言上。一般我们要将c++/c移植到别的语言上,基本操作是将c++中的实现代码转变成dll,然后再供别的语言调用,这样安全性高,且易于调用。但是因为各种语言不相同,如果不借助swig,自己去封装。在调用dll时就会有很多类型(包括基本类型,结构体和类)转换需要注意,比如c#调c++ dll,可能得自己造一个相对应与c++的结构体和类才能成功调用。但是如果我们用了swig,这些我们都不用考虑,我们只需要考虑swig给我们的接口类型是什么,然后我们只要按照swig给的接口类型传入参数,就OK了。说白了就是swig代替了我们利用c#去重新定义dll中所需要传入的参数类型(如结构体或类)这个工作。
1 一个构建接口的策略
1.1 为SWIG编写C语言程序
SWIG 不需要修改你的 C 代码,但如果你提供原始 C 头文件或源代码的集合,结果可能不是你所期望的——实际上,它们可能很糟糕。以下是为 C 程序创建接口时可以遵循的一系列步骤:
- 确定要包装的函数。可能没有必要访问 C 程序的每个函数——因此,一点预先的考虑可以大大简化最终的脚本语言接口。查找要包装的东西的话,C 头文件是特别好的源头。
- 创建一个新的接口文件来描述程序的脚本语言接口。
- 将适当的声明复制到接口文件中,或使用 SWIG 的 %include 指令处理整个 C 源/头文件。
- 确保接口文件中的所有内容都使用ISO C/ c++语法。
- 确保所有必要的’ typedef’声明和类型信息在接口文件中可用。特别是,确保按照 C/C++ 编译器的要求以正确的顺序指定类型信息。最重要的是,在使用类型之前定义它! 如果需要完整的类型信息,C编译器会告诉您,而SWIG通常不会发出警告或出错,因为它被设计为在没有完整类型信息的情况下工作。但是,如果未正确指定类型信息,则包装器可能是次优的,甚至会导致无法编译的 C/C++ 代码。
- 如果你的程序具有 main() 函数,则可能需要重命名。
- 运行 SWIG 并编译。
1.2 SWIG接口文件
使用 SWIG 的首选方法是生成单独的接口文件。假设你有以下 C 头文件:
/* File : header.h */
#include <stdio.h>
#include <math.h>
extern int foo(double);
extern double bar(int, int);
extern void dump(FILE *f);
此头文件的典型 SWIG 接口文件如下所示:
/* File : interface.i */
%module mymodule
%{
#include "header.h"
%}
extern int foo(double);
extern double bar(int, int);
extern void dump(FILE *f);
当然,在这种情况下,我们的头文件非常简单,所以我们可以使用更简单的方法并使用这样的接口文件:
/* File : interface.i */
%module mymodule
%{
#include "header.h"
%}
%include "header.h"
这种方法的主要优点是当将来头文件发生变化时,接口文件的维护最少。在更复杂的项目中,包含大量%include和#include语句的接口文件是最常见的接口文件设计方法之一,因为维护开销更低。
1.3 为什么使用单独的接口文件?
虽然 SWIG 可以解析许多头文件,但更常见的是编写一个特殊的 .i 文件来定义包的接口。你希望这样做的可能原因有以下几种:
- 很少需要访问大型包中的每个功能。许多 C 函数可能在脚本环境中很少或没有用处。因此,为什么要包装它们?
- 单独的接口文件提供了一个机会,可以提供有关如何构造接口的更精确的规则。
- 接口文件可以提供更多的结构和组织。
- SWIG 无法解析头文件中出现的某些定义。拥有单独的文件可以消除或解决这些问题。
- 接口文件提供了更精确的接口定义。想要扩展系统的用户可以转到接口文件,并立即查看可用的内容,而无需从头文件中挖掘。
1.4 获取正确的头文件
有时,为了使SWIG生成的代码能够正确编译,需要使用某些头文件。使用%{ %}块确保包含某些头文件
1.5 如何处理main()函数
大多数脚本语言都定义自己的main()过程,然后调用它。在处理动态加载时,Main()也没有意义。有几种方法可以解决main()冲突:
- 完全摆脱main()。
- 将main()重命名为其他名称。可以通过使用-Dmain=oldmain这样的选项来编译C程序。
- 当不使用脚本语言时,使用条件编译来只包含main()。
取消main()可能会导致程序潜在的初始化问题。为了处理这个问题,您可以考虑编写一个名为program_init()的特殊函数,该函数在程序启动时初始化程序。然后可以从脚本语言中作为第一个操作调用该函数,或者在加载SWIG生成的模块时调用该函数。
2 SWIG封装C#API实例
本实例将C++代码封装成C#的API。
2.1 创建工程与属性配置
- 首先,我们创建一个C++空项目,命名为SWIG_TEST。将项目编译环境设置为Release、x64,在工程项目属性中,设置目标文件扩展名和配置类型为dll,要注意的是C++项目所配置的版本与平台类型要与C#项目中使用的一致,因为不兼容可能会导致编译问题或不可预测的行为。这里是Release版本x64,那么就要保持版本一致。
- 添加接口文件,swig的接口文件(.i)实际上就是VS中的Midl文件(.idl)。
添加->新建项->Visual C++/代码/Midl文件。文件命名为SWIG_TEST.idl。注意,在工程项目新添加的idl文件的文件属性中,设置项目类型为自定义生成工具(或者设置为不参与生成)。实现C++接口后,可以编辑该接口文件,来指引SWIG生成API包装器代码。
- 创建C# 控制台程序工程,命名为SWIG_CSharp,Release版本x64,用于测试C++封装的C# API接口。新建的控制台工程有一个默认的Program.cs类文件,测试代码就写在main()函数中。
然后在工程中创建类文件,添加接口定义与实现。我们先提供一个代码量很小的接口实现,然后再不断复杂化接口,以便我们探索SWIG对各种C++类型代码的C# API封装。
以下提供了几个C#接口封装实现,都是在2.1的配置基础上进行的。
2.2 C#接口实现(一)
- C++工程中接口实现
// gmeDefine.h
#pragma once
#include <string>
#include <vector>
using namespace std;
namespace Smart3dMap
{
struct point3d{
point3d() { point3d(0, 0, 0);};
point3d(double xx, double yy, double zz){
x = xx; y = yy; z = zz;
};
double x; double y; double z;
};
struct triangle{
int a; int b; int c;
};
typedef vector<point3d> vec3dList;
typedef vector<triangle> vecTriList;
}
// gmeParseObj.h
#pragma once
#include "gmeDefine.h"
namespace Smart3dMap
{
class gmeParseObj
{
public:
gmeParseObj();
~gmeParseObj();
void fillPoints(vec3dList pnts);
void fillTriangles(vecTriList tris);
void setObjName(string name);
int getPointsNum();
int getTrianglesNum();
string getName();
vector<int> getBatchId();
private:
vec3dList m_points;
vecTriList m_triangles;
string m_name;
vector<int> m_bachId;
};
}
// gmeParseObj.cpp
#include "gmeParseObj.h"
Smart3dMap::gmeParseObj::gmeParseObj()
{
point3d pnt1 = point3d(1, 2, 3);
point3d pnt2 = point3d(1, 2, 3);
m_points.push_back(pnt1);
m_points.push_back(pnt2);
}
Smart3dMap::gmeParseObj::~gmeParseObj()
{
}
void Smart3dMap::gmeParseObj::fillPoints(vec3dList pnts)
{
m_points.assign(pnts.begin(), pnts.end());
}
void Smart3dMap::gmeParseObj::fillTriangles(vecTriList tris)
{
m_triangles.assign(tris.begin(), tris.end());
}
void Smart3dMap::gmeParseObj::setObjName(string name)
{
m_name = name;
}
int Smart3dMap::gmeParseObj::getPointsNum()
{
return m_points.size();
}
int Smart3dMap::gmeParseObj::getTrianglesNum()
{
return m_triangles.size();
}
string Smart3dMap::gmeParseObj::getName()
{
return m_name;
}
vector<int> Smart3dMap::gmeParseObj::getBatchId()
{
return m_bachId;
}
// SWIG_TEST.idl
%module SWIG_TEST
%{
#include "gmeDefine.h"
#include "gmeParseObj.h"
%}
%include "std_string.i"
using namespace std;
%include "gmeDefine.h"
%include "gmeParseObj.h"
添加以上接口实现文件后,在VS工程中编译通过。打开到该C++工程的代码目录下,进入命令行,输入命令
E:\C++_Code\code_2022_7_24\SWIG_TEST>E:\swigwin-4.0.2\swig.exe -csharp -c++ SWIG_TEST.idl
然后在该目录下会编译生成一系列的C# .cs文件,以及C++代码包装器文件SWIG_TEST_wrap.cxx 。
将SWIG_TEST_wrap.cxx 文件添加到SWIG_TEST工程,然后重新生成。将.cs文件以及编译生成的dll和lib文件加入到SWIG_CSharp工程目录下。
2. C#主函数调用
将SWIG生成的.cs文件添加到工程。
// Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SWIG_CSharp
{
class Program
{
static void Main(string[] args)
{
gmeParseObj obj = new gmeParseObj();
string fname = "obj_1";
obj.setObjName(fname);
int pntsNum = obj.getPointsNum();
Console.WriteLine(fname + "内有" + pntsNum.ToString() + "个点数据");
Console.ReadKey();
}
}
}
运行结果,接口运行正常。
3. 分析
在SWIG_TEST工程代码中,一共定义了2个结构体和1个类,引用了C++标准库 string和 vector库,是一个非常简单的实例。可以看到在接口文件中,使用了%include "gmeDefine.h"和%include “gmeParseObj.h”,直接让SWIG处理整个头文件,这样SWIG为头文件中的每一个类和函数都生成了包装器代码。
我们可以看到每一个C++接口函数都生成了C#的托管,在最后的调用中,调用了gmeParseObj类的构造函数,实现了初始化;
调用了setObjName(string )接口,这里可以直接传入string类型的函数参数,是因为在接口文件中使用了指令 %include “std_string.i”;using namespace std;将C++中的string类型与C#中的string类型进行了链接。如果是默认情况下,SWIG会自动为string类型生成包装器。
public void setObjName(string name)
{
SWIG_TESTPINVOKE.gmeParseObj_setObjName(swigCPtr, name);
if (SWIG_TESTPINVOKE.SWIGPendingException.Pending) throw SWIG_TESTPINVOKE.SWIGPendingException.Retrieve();
}
C++头文件中的接口 void fillPoints(vec3dList pnts); 在C#托管代码中定义如下:
public void fillPoints(SWIGTYPE_p_vectorT_Smart3dMap__point3d_t pnts) {
SWIG_TESTPINVOKE.gmeParseObj_fillPoints(swigCPtr, SWIGTYPE_p_vectorT_Smart3dMap__point3d_t.getCPtr(pnts));
if (SWIG_TESTPINVOKE.SWIGPendingException.Pending) throw SWIG_TESTPINVOKE.SWIGPendingException.Retrieve();
}
可以看到接口形参vec3dList类型( typedef vector vec3dList;),被 SWIGTYPE_p_vectorT_Smart3dMap__point3d_t 重新包装了。这样也许不是我们想要的,这样会给C#调用接口产生困难,试着做一点改变,将接口文件修改一下:
%module SWIG_TEST
%{
#include "gmeDefine.h"
#include "gmeParseObj.h"
using namespace Smart3dMap;
%}
%include <std_string.i>
%include <std_vector.i>
using namespace std;
%include "gmeDefine.h"
%template(vec3ds) vector<Smart3dMap::point3d>;
%template(vecTris) vector<Smart3dMap::triangle>;
%include "gmeParseObj.h"
代码块%{……%}中包含的部分,SWIG不会解析,而是逐字复制到包装器代码中,如上所示,如果不添加using namespace Smart3dMap; 你会发现在工程中添加包装器代码文件SWIG_TEST_wrap.cxx后,无法编译通过,总会出现未找到相关定义的问题,这是因为没有加入命名空间,无法引用命名空间下定义的结构体和类。在%template(vec3ds) vectorSmart3dMap::point3d指令中,特别要注意加入命名空间,否则SWIG找不到point3d结构体的定义,就会自己默认生成相应的包装类了,总之SWIG并不是那么智能,像等价替换之类都是不能识别的,要注意给SWIG提供明确直接的定义。
如此编译通过后,将生成的相应接口文件拷贝到C#工程目录下,添加调用接口代码:
// Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SWIG_CSharp
{
class Program
{
static void Main(string[] args)
{
point3d pd_1 = new point3d(2, 3, 4);
point3d pd_2 = new point3d(3, 4, 5);
point3d pd_3 = new point3d(6, 7, 2);
gmeParseObj obj = new gmeParseObj();
string fname = "obj_1";
obj.setObjName(fname);
vec3ds points = new vec3ds();
points.Add(pd_1);
points.Add(pd_2);
points.Add(pd_3);
obj.fillPoints(points);
int pntsNum = obj.getPointsNum();
Console.WriteLine(fname + "内有" + pntsNum.ToString() + "个点数据");
Console.ReadKey();
}
}
}
所有接口就都可以正常调用了。
最后在说明一下文件拷贝的细节:
4. C++工程SWIG_TEST:
将c++工程目录下的SWIG_TEST\x64\Release中生成的动态库和静态库文件拷贝到C#工程相应的SWIG_CSharp\bin\x64\Release目录下。注意动态库的名字要与接口文件中的%module SWIG_TEST指令中的名字相同,否则在编译C#工程时,会出现找不到动态库和无法初始化的错误。
将生成的C#文件拷贝到C#工程目录下,并添加到工程
2.2 C#接口实现(二)
本实例实现一个坐标转换的简单接口,涉及到虚函数,以及需要调用第三方库的接口封装。
将第三方库proj的头文件(proj.h,proj_api.h)、动态库dll(proj_7_1.dll)、静态库lib(proj.lib),加入到工程项目的环境目录中,并在工程属性中配置加载静态库的路径。
文件,定义接口。
//gmeDefine.h
#pragma once
#include <string>
#include <vector>
using namespace std;
namespace Smart3dMap
{
struct point3d
{
point3d()
{
point3d(0, 0, 0);
};
point3d(double xx, double yy, double zz)
{
x = xx;
y = yy;
z = zz;
};
point3d operator+ (const point3d& vec)
{
double nx = x + vec.x;
double ny = y + vec.y;
double nz = z + vec.z;
return point3d(nx, ny , nz);
};
double x;
double y;
double z;
};
}
// projTrans.h
#pragma once
#define ACCEPT_USE_OF_DEPRECATED_PROJ_API_H //使用Proj4库需要预定义的宏
#include "gmeDefine.h"
using namespace std;
namespace Smart3dMap
{
class projBase
{
public:
projBase();
virtual ~projBase();
virtual point3d projTrans(const point3d& vec) { return point3d(0, 0, 0); };
};
class projMethod1 :public projBase //WGS84 转 CGCS2000 高斯克吕格3度带 中央经线114
{
public:
point3d projTrans(const point3d& vec);
};
class projTrans
{
public:
point3d coordinateConv(const point3d& vec, projBase* projMethod);
};
}
//projTrans.cpp
#include "projTrans.h"
#include "proj.h"
#include "proj_api.h"
namespace Smart3dMap
{
point3d projTrans::coordinateConv(const point3d & vec, projBase * projMethod)
{
return projMethod->projTrans(vec);
}
point3d projMethod1::projTrans(const point3d & vec)//wgs84 to cgcs2000
{
///113.52002678285267, 28.175856297122078, 232.68600000000001
//度转化为弧度制
double x = vec.x * DEG_TO_RAD;
double y = vec.y * DEG_TO_RAD;
double z = vec.z;
//proj4库投影参数
const char* cgcs2000 = "+proj=tmerc +lat_0=0 +lon_0=114 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs ";//CGCS2000不加带号,中央经线114,3度分带
const char *wgs84 = "+proj=longlat +datum=WGS84 +no_defs ";//wgs84
projPJ pj_src = pj_init_plus(wgs84); //目标投影
projPJ pj_dst = pj_init_plus(cgcs2000);//源投影
//坐标投影变换
pj_transform(pj_src, pj_dst, 1, 1, &x, &y, &z);
pj_free(pj_src); //销毁投影指针
pj_free(pj_dst);
return point3d(x, y, z);
}
projBase::projBase()
{
}
projBase::~projBase()
{
}
}
//SWIG_TEST.idl
%module(directors = "1") SWIG_TEST
%{
#include "gmeDefine.h"
#include "projTrans.h"
using namespace Smart3dMap;
%}
%include <std_string.i>
%include <std_vector.i>
using namespace std;
%rename(add_point) Smart3dMap::point3d::operator + (const point3d &);
%include "gmeDefine.h"
%feature("director") Smart3dMap::projBase;
%include "projTrans.h"
//Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SWIG_CSharp
{
class Program
{
static void Main(string[] args)
{
point3d pd_1 = new point3d(113.520, 28.176, 232.686);
projTrans projTool = new projTrans();
projMethod1 projMethod_wgs84ToCGCS2000 = new projMethod1();
point3d pntOut = projTool.coordinateConv(pd_1, projMethod_wgs84ToCGCS2000);
Console.WriteLine("WGS84坐标:" + pd_1.x.ToString() + " " + pd_1.y.ToString() + " " + pd_1.z.ToString() +
" 转换为CGCS2000坐标:" + pntOut.x.ToString() + " " + pntOut.y.ToString() + " " + pntOut.z.ToString());
point3d pd_2 = new point3d(10, 15, 25);
point3d pd_3 = new point3d(12, 16, 27);
point3d pd_Add = pd_2.add_point(pd_3);
Console.WriteLine("坐标1:" + pd_2.x.ToString() + " " + pd_2.y.ToString() + " " + pd_2.z.ToString() +
" + 坐标2:" + pd_3.x.ToString() + " " + pd_3.y.ToString() + " " + pd_3.z.ToString() + " = "
+ pd_Add.x.ToString() + " " + pd_Add.y.ToString() + " " + pd_Add.z.ToString());
Console.ReadKey();
}
}
}
运行结果
分析:%module(directors = “1”)指令与%feature(“director”) Smart3dMap::projBase启动了director,允许基类projBase中的虚函数在子类中重写,从而实现多态。
gmeDefine.h中还在结构体中实现了一个重载操作符的函数,重载操作符函数名在大多数脚本语言中是非法标识符,所以必须对其进行重命名才能为其构建包装器,在接口函数中调用%rename(add_point) Smart3dMap::point3d::operator + (const point3d &)指令,创建add_point函数实现相同功能。