许多人都知道MapGuide提供了.NET、PHP和Java三种类型的Web API,但是不知道MapGuide是如何创建这三种类型的API的。试想一下,如果分别去创建这三种API,这将是一个很难维护的工作。每次增加或修改一些功能,就需要对三种类型的API都进行修改。所以,MapGuide使用了SWIG来自动生成这三种类型的API。我想这个时候许多人会问,什么是SWIG呢?我怎么从来没有听说过这个东东呢!其实,我也是在做MapGuide开发的时候才开始了解SWIG的。所以,首先让我们来认识一下SWIG,然后再来看MapGuide是如何使用SWIG来生成API的。
1. SWIG简介
SWIG是Simple Wrapper and Interface Generator的缩写,是一个帮助使用C或者C++编写的软件创建其他编语言的API的工具。例如,我想要为一个C++编写的程序创建.NET API,一般情况下我必须使用托管C++(Managed C++)去编写大量的代码才能生成它的.NET API。有了SWIG,这个机械的工作将变得非常简单。你只须要使用一个接口文件告诉SWIG要为那些类创建.NET API,SWIG就会自动帮你生成它的.NET API。是不是非常的酷啊?
当然,SWIG不仅仅支持创建.NET API。最新版本的SWIG支持常用脚本语言Perl、PHP、Python、Tcl、Ruby和非脚本语言C#, Common Lisp (CLISP, Allegro CL, CFFI, UFFI), Java, Modula-3, OCAML以及R,甚至是编译器或者汇编的计划应用(Guile, MzScheme, Chicken)。
下面我们通过一个例子来看看SWIG是如何帮我们创建API的。假设我打算为如下的C++类创建C#和Java的API。
/* SwigTest.h */
class CSwigTest {
public:
CSwigTest();
virtual ~CSwigTest();
int Add(int a, int b) { return a + b; }
int Substract(int a, int b) { return a - b; }
int Multiple(int a, int b) { return a * b; }
float Divide(int a, int b) { return (float)a / (float)b; }
};
1.1 接口文件
首先,你需要写一个接口文件(Interface File),告诉SWIG要为那些类的那些方法创建API。如下的接口文件只为类CSwigTest的方法Add(...)和Subtract(...)生成API,因为在接口文件的接口声明部分只声明了两个方法。
/* SwigTest.i */
%module SwigTest
%{
#include "SwigTest.h"
%}
/* --- 接口声明部分 ---*/
class CSwigTest {
public:
int Add(int a, int b);
int Substract(int a, int b);
};
注解:%module标记用于定义SWIG生成的模块的名称,%{%}标记中的内容会被一字不差地插入SWIG自动生成的文件xxx_wrapper.c中,其中xxx代表用%module指定的模块名称。这个文件会在下面介绍,不必着急去理解它究竟有什么作用。
如果打算为类中所有方法创建API,那么有一个非常简单的办法,在接口文件的类声明部分使用%include标记。SWIG将对%include所指定的文件进行语法分析,类中所有公有方法(Public Method)都将在API中暴露。
/* SwigTest.i */
%module SwigTest
%{
#include "SwigTest.h"
%}
#include “SwigTest.h”
1.2 编译模块
有了接口文件之后,剩下的事就是执行几条命令。下面我们以Windows平台上生成.NET API为例介绍这些命令。
(a) 调用SWIG自动生成代码
swig -csharp SwigTest.i
执行上面的命令会产生一个C语言文件SwigTest_wrapper.c和多个C#文件。在文件SwigTest_wrapper.c中,SWIG为接口文件中接口声明部分指定的每个方法产生一个全局方法,以便C#使用Pinvoke调用这些函数。而那些C#文件就是用来生成.NET API的。
(b) 为C++代码生成DLL(动态链接库)
cl SwigTest_wrapper.c *.cpp
link *.obj /out:SwigTest.dll
执行上面的命令,会为我们编写C++代码生成DLL。在编译C++文件时,一定要包括SWIG为我们生成的C++文件SwigTest_wrapper.cpp。
注意:为了让大家便于理解上述命令,这些命令并没有列出完整的编译和链接选项。
(C) 生成.NET模块
csc /out:SwigTestNotNetAPI.dll /target:library *.cs
执行上面的命令就生成了.NET API模块SwigTestNotNetAPI.dll。如果用户想使用这些API,只需要添加对SwigTestNotNetAPI.dll的引用(Reference)就可以了。
生成其它语言类型API的命令基本类似,下面我们再以Java在Unix平台下的命令为例结束对SWIG的介绍。事实上,SWIG也是一个开源项目。如果想了解更多关于SWIG的信息,大家可以登陆SWIG的官方网站www.swig.org,那里有SWIG最详细的资料。
$ swig -java SwigTest_wrapper.i
$ gcc -c *.cpp SwigTest_wrapper.c -I/c/jdk1.3.1/include -I/c/jdk1.3.1/include/win32
$ gcc -shared *.o -mno-cygwin -Wl,--add-stdcall-alias -o SwigTest.dll
2. SWIG在MapGuide中的应用
我们在前面已经提到过,MapGuide使用了SWIG来自动生成.NET、Java和PHP这三种类型的API。但是,SWIG也有不少限制和缺陷,所以MapGuide对SWIG源代码进行了大量的修改,以满足自己的要求。下面,我们看看这些改进。
2.1 IMake工具
SWIG要求开发人员编写一个接口文件,那么能否让接口文件自动生成呢?借用一句中国移动的广告词,我能!虽然SWIG没有提供这方面的工具,但是我们可以自己开发吗!IMake(Interface Maker)就是为了满足这样的要求而开发一个工具,给定一个XML文件,它能帮你自动生成SWIG接口文件。登录MapGuide开源版的代码浏览页面(http://trac.osgeo.org/mapguide/browser),在root/trunk/MgDev/BuildTools/WebTools/IMake文件夹下可以找到IMake的源代码。
下面我们以MapGuide中使用的XML文件/trunk/MgDev/Web/src/MapGuideApi/MapGuideApiGen.xml为例,介绍一下IMake的用法。为了便于理解,在此我删掉了文件中的部分内容。
<?xml version="1.0" encoding="UTF-8"?>
<Parameters>
<!-- 对应于%Module标记. -->
<Module name="MapGuideApi" />
<!-- 生成的接口文件的名称. -->
<Target path="./MapGuideApi.i" />
<!-- 对应于%{%}标记 -->
<CppInline>
#include <string>
#include <map>
#include "MapGuideCommon.h"
#include "WebApp.h"
......
</CppInline>
<!-- 用于替换接口中使用的部分类型 -->
<TypeReplacements>
<TypeReplacement oldtype="CREFSTRING" newtype="STRINGPARAM" />
<TypeReplacement oldtype="INT64" newtype="long long" />
</TypeReplacements>
<!-- 此部分的内容添加在%{%}之后,接口声明部分之前 -->
<SwigInline>
%include "language.i" //typemaps specific for each language
......
</SwigInline>
<!-- 为指定的C++文件生成接口声明 -->
<Headers>
<Header path="../../../Common/Foundation/Data/Property.h" />
......
</Headers>
</Parameters>
执行命令“IMake MapGuideApiGen.xml”,IMake就帮我们自动生成了如下SWIG接口文件MapGuideApi.i。
/* MapGuideApi.i */
%module MapGuideApi
%{
#include <string>;
#include <map>;
#include "MapGuideCommon.h"
#include "WebApp.h"
......
%}
%include "language.i" //typemaps specific for each language
......
class MgProperty: public MgNamedSerializable
{
public:
virtual INT16 GetPropertyType();
STRING GetName();
void SetName(CREFSTRING name);
};
......
如果打开文件Proper.h,我们可以看到MgProperty有更多的方法,例如CanSetName(...)。为什么只有三个方法添加到了SWIG接口文件中?IMake在生成接口文件时,它会查找C++头文件中的宏PUBLISHED_API。只有被PUBLISHED_API修饰的方法,才会添加到接口文件中。
注:宏PUBLISHED_API和INTERNAL_API的定义如下。
#define PUBLISHED_API public
#define INTERNAL_API public
class MG_FOUNDATION_API MgProperty : public MgNamedSerializable
{
PUBLISHED_API:
virtual INT16 GetPropertyType() = 0; /// __get
STRING GetName(); /// __get, __set
void SetName(CREFSTRING name);
INTERNAL_API:
virtual bool CanSetName();
protected:
INT32 GetClassId();
MgProperty();
virtual ~MgProperty();
virtual void Dispose();
virtual void ToXml(string &str, bool includeType = true, string rootElmName = "Property") = 0;
private:
friend class MgPropertyCollection;
STRING m_propertyName;
CLASS_ID:
static const INT32 m_cls_id = Foundation_Property_Property;
};
给定一个C++常量定义文件,IMake还可以自动生成对应的其他语言的常量定义文件。MapGuide .NET Web API中的所有常量都是使用IMake来生成的,例如MgMineType、MgPropertyType等。下面我们以MapGuide中使用的XML文件/trunk/MgDev/Web/src/MapGuideApi/Constants.xml为例,介绍如何自动生成各种语言的常量定义文件。同样,为了便于理解,在此我删掉了文件中的部分内容。与MapGuideApiGen.xml不同,Constants.xml包含一个新的元素Classes用来指出需要在目标语言中产生对应的常量类的C++类。
<?xml version="1.0" encoding="UTF-8"?>
<Parameters>
<!-- 用于替换类型 -->
<PHPTypeReplacements>
<TypeReplacement oldtype="STRING" newtype="" />
<TypeReplacement oldtype="INT16" newtype="" />
......
</PHPTypeReplacements>
<CSharpTypeReplacements>
<TypeReplacement oldtype="STRING" newtype="string" />
<TypeReplacement oldtype="INT16" newtype="short" />
......
</CSharpTypeReplacements>
<JavaTypeReplacements>
<TypeReplacement oldtype="STRING" newtype="String" />
<TypeReplacement oldtype="INT16" newtype="short" />
......
</JavaTypeReplacements>
<Namespace>OSGeo.MapGuide</Namespace>
<Package>org.osgeo.mapguide</Package>
<!-- 用于指出需要在目标语言中产生对应的常量类的C++类 -->
<Classes>
<Class name="MgMineType" />
<Class name="MgPropertyType" />
......
</Classes>
<Headers>
<Header path="../../../Common/Foundation/Data/MimeType.h" />
<Header path="../../../Common/Foundation/Data/PropertyType.h" />
......
</Headers>
</Parameters>
执行命令“IMake.exe Constants.xml C# Constants.cs”,IMake就帮我们自动生成了一个C#常量文件Constants.cs。对于文件/trunk/MgDev/Common/Foundation/Data/PropertyType.h中定义了如下常量,
class MgPropertyType
{
PUBLISHED_API:
static const int Null = 0;
static const int Boolean = 1;
static const int Byte = 2;
static const int DateTime = 3;
static const int Single = 4;
......
};
在生成的Constants.cs文件中,有如下的类定义。
class MgPropertyType
{
static const int Null = 0;
static const int Boolean = 1;
static const int Byte = 2;
static const int DateTime = 3;
static const int Single = 4;
......
};
这个文件可以被C#的编译器直接编译,所以MapGuide没有使用SWIG生成常量的API,而是直接使用IMake。 如果想生成PHP或Java的常量定义文件,只需要将IMake命令的参数"C#"替换为"PHP"或"Jave"就可以了。
2.2 MapGuide对SWIG的修改
在MapGuide开始使用SWIG的时候,可用的SWIG的最高版本是1.3.21,从那以后MapGuide在没有升级过SWIG。所以,到现在为止,MapGuide的SWIG版本仍然是1.3.21。这个版本的SWIG有不少限制和缺陷,
- 无法创建基于自定义根异常类MgException的异常处理机制。
- 无法创建属性(Property)。
- 对某些方法无法产生正确的API。例如,如果方法GetA(...)返回的是类A的子类B的实例,SWIG创建的API返回的仍然是A类的实例。此时如果你把返回值转换为类B,那么转换会失败。
A* GetA();
- ......
事实上最新的SWIG版本也没有全部解决这些问题,所以MapGuide对SWIG源代码进行了大量的修改,以满足自己的要求。看看MapGuide在使用SWIG命令是传入的参数,我们可以发现有许多参数不是SWIG标准的参数,例如proxydir、clsidcode、clsiddata、catchallcode等。
swig -c++ -csharp -dllname MapGuideUnmanagedApid -namespace OSGeo.MapGuide -proxydir .\custom -baseexception MgException -clsidcode getclassid.code -clsiddata m_cls_id -catchallcode catchall.code -dispose "((MgDisposable*)arg1)->Release()" -rethrow "e->Raise();" -nodefault -noconstants -module MapGuideApi -o MgApi_wrap.cpp -lib ..\..\..\Oem\SWIGEx\Lib MapGuideApi.i
在此,我们不打算一一介绍这些参数,因为在多数情况下你没有必要对了解参数的含义。我们只介绍MapGuide是如何来解决上述SWIG的第二和第三个问题的,因为在扩展MapGuide Web API的时候你可能会用得着。
2.2.1 创建属性
如果你看过MapGuide源代码的话,你会发现有许多方法声明之后有“__get”、“__set”或“__get, __set”这样的注释,如类MgProperty中的方法。
class MgProperty : public MgNamedSerializable
{
PUBLISHED_API:
virtual INT16 GetPropertyType() = 0; /// __get
STRING GetName(); /// __get, __set
void SetName(CREFSTRING name);
......
};
这些注释是有特殊含义的,它们就是用来解决上述SWIG的第二个问题的。当IMake工具扫描C++头文件时发现这注释后,会在目录“.\custom”下为每个类产生一个帮助创建属性的代码文件。例如,如果要类MgProperty生成.NET API,IMake会在“.\custom”生成一个文件名为MgProperty的C#代码文件,它的内容如下:
public int PropertyType {
get {return GetPropertyType(); }
}
public int Name {
get { return GetPropertyType(); }
set { setName(value);}
}
如果在SWIG的命令行中使用了参数proxydir,那么SWIG在为每个类生成代码的时候,会在proxydir所指定的目录下查找和类名相同的文件,并且将这个文件中的代码插入类的目标代码中。通过这种办法,就解决了上述SWIG的第二个问题。
2.2.2 ClassId
MapGuide Web API中的所有类都是从MgObject继承而来的,在类MgObject中有一个方法GetClassId()用来返回每个类唯一的ID值。MapGuide就是用这个方法来解决上述SWIG的第三个问题的,所以如果要在MapGuide Web API中增加一个新类,一定要覆盖(override)这个方法,并且提供一个唯一的ID值。
class MgObject
{
EXTERNAL_API:
virtual INT32 GetClassId();
virtual STRING GetClassName();
INTERNAL_API:
virtual ~MgObject();
bool IsOfClass(INT32 classId);
};
3. 扩展MapGudie Web API
如果你发现现有的MapGuide Web API无法满足你的要求,没有关系,你可以去尝试扩展它,因为MapGuide是开源的。
如果要新添类,基本步骤如下:
(a) 修改C++代码,添加新的类。对于需要暴露于API的方法,使用宏PUBLISHED_API修饰。
(b) 修改XML文件/trunk/MgDev/Web/src/MapGuideApi/MapGuideApiGen.xml的Headers部分,为每个新添加类所在的C++头文件增加一个Header元素。下面的示例中,"path"代表C++头文件的路径,"filename.h"代表文件的名称。
<Headers>
<Header path="path/filename.h" />
......
</Headers>
(c) 重新编译MapGuide的Web模块(/trunk/MgDev/Web/src/)。
如果要增加一些新的方法到现有的类中,基本步骤如下:
(a) 修改C++代码,添加新的方法,并且使用宏PUBLISHED_API修饰这些方法。
(b) 重新编译MapGuide的Web模块(/trunk/MgDev/Web/src/)。
如果要新增常量类,基本步骤如下:
(a) 修改C++代码,添加新的常量类。
(b) 修改XML文件/trunk/MgDev/Web/src/MapGuideApi/Constants.xml,在Classes部分为每个新添加常量类增加一个Class元素,在Headers部分为每个新添加常量类所在的C++头文件增加一个Header元素。下面的示例中,"ClassName"代表新添加的C++常量类的名称。
<Classes>
<Class name="ClassName" />
......
</Classes>
<Headers>
<Header path="path/filename.h" />
......
</Headers>
(c) 重新编译MapGuide的Web模块(/trunk/MgDev/Web/src/)。