1.引言
Python或Java调用MATLAB程序需要安装MATLAB官方提供的支持库(MATLAB Runtime),而且适配的python或JDK版本有限,不方便移植。本文的思路是用MATLAB Coder将MATLAB源程序转为C/C++代码,然后用swig将其打包成python 动态库pyd或java dll,从而避免程序移植时需要安装MATLAB支持库。
2.swig安装
首先在swig官网下载swig:https://www.swig.org/download.html.
选择最新版本4.1.1。
如果是windows记得选择下载swigwin-4.1.1。下载完成后解压得到以下目录:
将该目录中的 swig.exe 所在路径添加至 系统变量Path 中:
这里将swigwin-4.1.1目录放在了D盘下。打开windows cmd,运行:
swig -version
出现以下内容则表示swig已经安装成功了:
SWIG Version 4.1.1
Compiled with i686-w64-mingw32-g++ [i686-w64-mingw32]
Configured options: +pcre
Please see https://www.swig.org for reporting bugs and further information
3.MATLAB 源程序
设需要封装的程序为静止圆球下,地心地固坐标系(ECEF)位置转经纬高(LLA)的函数ECEF2LLA.m:
function LLA=ECEF2LLA(ECEFPos)
% 地球固连坐标系到经纬高坐标系位置
x=ECEFPos.x;
y=ECEFPos.y;
z=ECEFPos.z;
E2=6.69438e-3;
Ra=6378137.0;
LLA=struct;
L=atan(y/x);
LLA.longtitude=180+rad2deg(L);
B=0; % 给纬度初始值,然后循环迭代求解,很快收敛
for m=1:20
N=Ra/sqrt(1-E2*sin(B)^2);
H=sqrt(x^2+y^2)/cos(B)-N;
B=atan(z*(N+H)/(sqrt(x^2+y^2)*(N+H-N*E2)));
LLA.latitude=rad2deg(B);
LLA.height=H;
end
end
该函数的调用示例程序main.m为:
ECEFPos=struct("x",0,"y",3e5,"z",200);
disp(ECEF2LLA(ECEFPos));
4.使用MATLAB coder生成C源码
4.1选择入口函数
在matlab命令行输入:
coder
打开代码生成器:
选择要入口函数(Entry-point Function)为ECEF2LLA:
注:点击Add Entry-Point Function 可以继续添加多个入口函数
4.2 定义函数参数和返回值类型
点击下一步,对函数参数和返回值进行定义。MATLAB可以根据入口函数的调用示例自动推导函数的参数和返回值类型。选择main,m脚本作为函数的调用示例,点击Autodefine Input Types自动推导类型:
点击 Add global可以添加为生成的C/C++代码添加全局变量,点击加号可以添加参数字段(如果有缺失),点击表格可以编辑参数字段。返回值类型则由MATLAB自动确定。
4.3 MATLAB源程序运行正确性检查
为了避免MATLAB源码存在问题导致生成的代码存在bug,MATLAB会先为要生成代码的MATLAB源程序创建MEX函数然后运行,以检查源程序中可能存在的问题。点击check for Issues检查代码:
4.4 代码生成
点击下一步,打开生成的C/C++程序设置界面:
选择构建类型为Source Code。注意:
生成的语言最好选择为C,因为CPython的解释器是用C实现的,选择C++可能会因为版本问题导致不兼容,使得生成的pyd不能被CPython的解释器加载。
点击Generate生成代码。
点击Next可以查看代码生成总结:
从Generate Output一栏可以看到生成的C代码在MATLAB源码目录的codegen\lib\ECEF2LLA子文件夹下;运行示例在codegen\lib\ECEF2LLA\examples内。
5.使用swig封装C源码为python动态库
5.1 编写swig接口文件
生成的C代码会缺少一个tmwtypes.h头文件,该文件在MATLAB安装目录的extern\include子文件内。例如MATLAB安装目录为:
D:\Matlab\extern\include
则该文件位置为:
D:\Matlab\extern\include
将该文件复制到生成的C代码目录内,这里即codegen\lib\ECEF2LLA。然后编写如下的swig接口文件
ECEF2LLA.i:
%module ECEF2LLA
%include<carrays.i>
%include<typemaps.i>
%{
// 引入源码中的头文件,以定义python接口文件中的数据类型和方法
#include "ECEF2LLA.h"
#include "ECEF2LLA_types.h"
%};
// 生成的python接口文件中的数据类型定义
typedef struct {
double x;
double y;
double z;
} struct0_T;
typedef struct {
double longtitude;
double latitude;
double height;
} struct1_T;
//接口文件提供给外部调用的函数声明
extern void ECEF2LLA(const struct0_T *ECEFPos, struct1_T *LLA);
注意:
(1)上述swig接口文件中的struct0_T,struct1_T和ECEF2LLA是要暴露给pyd调用者的数据类型和方法,其中struct0_T,struct1_T在ECEF2LLA_types.h中定义,ECEF2LLA在ECEF2LLA.h中定义。(2)swig接口文件中的%{...%}块表示引入源码中的内容。
5.2 新建CMake工程
使用Clion在MATLAB生成的C源码目录codegen\lib\ECEF2LLA中新建一个项目,添加如下一个CMakeLists.txt文件,内容如下:
#[[ CMakeLists.txt ]]
#指定CMake的最小版本
cmake_minimum_required(VERSION 3.17)
#CMake项目名称,要和swig module名称一致
project(ECEF2LLA)
#C++标准,这里设置为C++20
set(CMAKE_CXX_STANDARD 20)
#指定你的.cxx等文件的目录
include_directories(${PROJECT_SOURCE_DIR})
#寻找安装好的swig,其实就是去电脑中找你安装好的Swig环境,所以我们需要提前安装环境。
find_package(SWIG REQUIRED)
include(${SWIG_USE_FILE})
#寻找python解释器和库
find_package(Python3 COMPONENTS Interpreter Development)
include_directories(${Python3_INCLUDE_DIRS})
link_libraries(${Python3_LIBRARIES})
#swig生成的接口文件存放位置
set(SWIG_OUTFILE_DIR ${CMAKE_CURRENT_BINARY_DIR}/python)
#查找.cpp, .c和.h文件并存到source_files变量中
file(GLOB source_files *.cpp *.c *.h)
#打印找到的文件到控制台
message("find fies: ${source_files}")
#设定生成的语言:java python等
set(LANGUAGE "python")
#swig 接口文件路径
set(SWIG_INTERFACE "ECEF2LLA.i")
#设置swig为C++模式
#SET_SOURCE_FILES_PROPERTIES(${SWIG_INTERFACE} PROPERTIES CPLUSPLUS ON)
#自动展开头文件
SET_SOURCE_FILES_PROPERTIES(${SWIG_INTERFACE} PROPERTIES SWIG_FLAGS "-includeall")
#swig生成指令,ECEF2LL即为ECEF2LLA.i中的swig module名称
swig_add_library(ECEF2LLA TYPE SHARED LANGUAGE ${LANGUAGE} SOURCES
${SWIG_INTERFACE}
${source_files}
)
然后点击Reload changes刷新项目:
5.3 生成pyd
点击Clion的Build按钮(右上角工具栏的绿色小锤子)构建项目,构建完成后打开生成的cmake-build-debug目录:
目录中有三个文件需要特别关注:
python/ECEF2LLAPYTHON_wrap.c: 这个就是swig生成的python程序C接口文件,它的位置就是在CMakeLists.txt中SWIG_OUTFILE_DIR(第24行)变量定义的路径。
_ECEF2LLA.pyd:这就是我们需要的Python动态库文件。注意这个文件不要重命名!!
ECEF2LLA.py:这是给python调用的接口文件。
打开ECEF2LLA.py可以看到包含如下内容:
class struct0_T(object):
...
class struct1_T(object):
...
def ECEF2LLA(ECEFPos, LLA):
return _ECEF2LLA.ECEF2LLA(ECEFPos, LLA)
这些就是在swig接口文件ECEF2LLA.i中定义的提供给外部程序调用的数据类型和函数。
5.4 运行pyd
复制_ECEF2LLA.pyd和ECEF2LLA.py到同一文件夹下,
新建main.py:
from ECEF2LLA import struct0_T, struct1_T, ECEF2LLA
if __name__ == '__main__':
ECEFPos = struct0_T()
ECEFPos.x = 0
ECEFPos.y = 3e5
ECEFPos.z = 200
LLA = struct1_T()
ECEF2LLA(ECEFPos, LLA)
print(LLA.longtitude, LLA.latitude, LLA.height)
运行main.py得到以下输出:
270.0 0.04453575351433897 -6078136.922270441
和MATLAB示例程序的输出一致:
说明MATLAB源程序已经正确封装为python库了。
5.5 更改pyd路径
如果需要更改pyd的位置,例如存到./dll子文件夹下:
此时运行main.py会报错:
此时只需要修改接口文件ECEF2LLA.py中的pyd导入路径即可。例如原来的pyd导入路径为:
# Import the low-level C/C++ module
if __package__ or "." in __name__:
from . import _ECEF2LLA
else:
import _ECEF2LLA
现在修改为:
# Import the low-level C/C++ module
if __package__ or "." in __name__:
from .dll import _ECEF2LLA
else:
import dll._ECEF2LLA as _ECEF2LLA
6.使用swig封装C源码为Java dll
6.1 使用swig生成Java dll 的CMakeLists.txt
使用swig封装C源码为Java dll的swig接口文件和之前是一样的,只用更改CMakeLists.txt文件的内容为生成java dll:
#[[ CMakeLists.txt ]]
#指定CMake的最小版本
cmake_minimum_required(VERSION 3.17)
#CMake项目名称,要和swig module名称一致
project(ECEF2LLA)
#C++标准,这里设置为C++20
set(CMAKE_CXX_STANDARD 20)
#指定你的.cxx等文件的目录
include_directories(${PROJECT_SOURCE_DIR})
#寻找安装好的swig,其实就是去电脑中找你安装好的Swig环境,所以我们需要提前安装环境。
find_package(SWIG REQUIRED)
include(${SWIG_USE_FILE})
#寻找jdk的库
include_directories($ENV{JAVA_HOME}/include)
include_directories($ENV{JAVA_HOME}/include/win32)
#swig输出文件存放位置
set(SWIG_OUTFILE_DIR ${CMAKE_CURRENT_BINARY_DIR}/java)
#查找.cpp, .c和.h文件并存到source_files变量中
file(GLOB source_files *.cpp *.c *.h)
#打印找到的文件到控制台
message("find fies: ${source_files}")
#设定生成的语言:java python等
set(LANGUAGE "java")
#swig 接口文件路径
set(SWIG_INTERFACE "ECEF2LLA.i")
#设置生成的dll java包名
set(JAVA_GEN_PACKAGE "com.demo.ecef2lla")
set(CMAKE_SWIG_FLAGS -package ${JAVA_GEN_PACKAGE} )
#设置swig为C++模式
#SET_SOURCE_FILES_PROPERTIES(${SWIG_INTERFACE} PROPERTIES CPLUSPLUS ON)
#自动展开头文件
SET_SOURCE_FILES_PROPERTIES(${SWIG_INTERFACE} PROPERTIES SWIG_FLAGS "-includeall")
#swig生成指令,ECEF2LL即为ECEF2LLA.i中的swig module名称
swig_add_library(ECEF2LLA TYPE SHARED LANGUAGE ${LANGUAGE} SOURCES
${SWIG_INTERFACE}
${source_files}
)
上述的CMakeLists.txt文件中有几处需要特别注意的地方:
(1)JDK库路径
上述CMakeLists.txt第20,21行中:
#寻找jdk的库
include_directories($ENV{JAVA_HOME}/include)
include_directories($ENV{JAVA_HOME}/include/win32)
JAVA_HOME是JDK安装时配置的环境变量:
(2)生成的dll中java包名
第37,38行中:
#设置生成的dll java包名
set(JAVA_GEN_PACKAGE "com.demo.ecef2lla")
set(CMAKE_SWIG_FLAGS -package ${JAVA_GEN_PACKAGE} )
设置生成的dll中java包名为com.demo.ecef2lla,这是因为dll中封装的JNI接口类权限为protected,所以必须指定包名,而且接口文件必须放在这个包名下才能被JDK访问。
6.2 使用Java调用dll
新建一个Java工程,在src下新建一个包com.demo.ecef2lla(必须和CMake中设置的一致),将cmake-build-debug目录下的.java和dll文件都复制到包内:
这些文件都不能重命名!!调用dll的Main.java为:
import com.demo.ecef2lla.ECEF2LLA;
import com.demo.ecef2lla.struct0_T;
import com.demo.ecef2lla.struct1_T;
public class Main {
public static void main(String[] args) {
System.loadLibrary("src/com/demo/ecef2lla/ECEF2LLA");
System.out.println("load lib success");
struct0_T ecefPos = new struct0_T();
ecefPos.setX(0);
ecefPos.setY(3e5);
ecefPos.setZ(200);
struct1_T LLA = new struct1_T();
ECEF2LLA.ECEF2LLA(ecefPos, LLA);
System.out.printf("%f,%f,%f%n", LLA.getLongtitude(), LLA.getLatitude(), LLA.getHeight());
}
}
运行结果为:
load lib success
270.000000,0.044536,-6078136.922270
和MATLAB示例程序的输出一致,说明Java dll已正确封装。
6.3 更改dll路径
如果需要更改dll的位置,需要将com.demo.ecef2lla包文件夹整个复制过去,例如放到src/dll内:
此时只需要更改System.loadLibrary中的路径为相应路径即可:
import com.demo.ecef2lla.ECEF2LLA;
import com.demo.ecef2lla.struct0_T;
import com.demo.ecef2lla.struct1_T;
public class Main {
public static void main(String[] args) {
System.loadLibrary("src/dll/com/demo/ecef2lla/ECEF2LLA");
...
}
}
7.常见问题
Q:MATLAB程序中调用了不支持生成C代码的MATLAB内置函数(如标准大气参数计算函数atmoscoesa)怎么办?
A: 自己用matlab将该函数实现一遍,替换内置的MATLAB函数。