SWIG学习记录(三)SWIG封装C#API实例


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 创建工程与属性配置

  1. 首先,我们创建一个C++空项目,命名为SWIG_TEST。将项目编译环境设置为Release、x64,在工程项目属性中,设置目标文件扩展名和配置类型为dll,要注意的是C++项目所配置的版本与平台类型要与C#项目中使用的一致,因为不兼容可能会导致编译问题或不可预测的行为。这里是Release版本x64,那么就要保持版本一致。
    在这里插入图片描述
  2. 添加接口文件,swig的接口文件(.i)实际上就是VS中的Midl文件(.idl)。
    添加->新建项->Visual C++/代码/Midl文件。文件命名为SWIG_TEST.idl。注意,在工程项目新添加的idl文件的文件属性中,设置项目类型为自定义生成工具(或者设置为不参与生成)。实现C++接口后,可以编辑该接口文件,来指引SWIG生成API包装器代码。
    在这里插入图片描述
  3. 创建C# 控制台程序工程,命名为SWIG_CSharp,Release版本x64,用于测试C++封装的C# API接口。新建的控制台工程有一个默认的Program.cs类文件,测试代码就写在main()函数中。
    在这里插入图片描述
    然后在工程中创建类文件,添加接口定义与实现。我们先提供一个代码量很小的接口实现,然后再不断复杂化接口,以便我们探索SWIG对各种C++类型代码的C# API封装。
    以下提供了几个C#接口封装实现,都是在2.1的配置基础上进行的。

2.2 C#接口实现(一)

  1. 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函数实现相同功能。

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值