MATLAB使用C/C++库
获取代码
本文中涉及的代码保存于:learnMex
C/C++库包括静态链接库(在Windows平台下为xxx.lib)和动态链接库(在Windows平台下为xxx.dll)两种。内部保存了可执行的二进制代码。如果需要在MATLAB中使用这些库,有两种基本方式:
- 使用
loadlibraryAPI。 - 使用mex编译器。
loadlibrary vs. mex
loadlibrary方式
dll文件由C/C++编译器生成。在生成时如何符合C语言规范并且对C/C++编译器设置了符号导出,则生成的dll文件中会包含可调用函数的名称和调用方式(参数列表和返回值)。loadlibrary便是基于这一特性实现的。其基本语法如下:
libname = 'foo';
loadlibrary(libname, [libname, '.h']);
[x1,...,xN] = calllib(libname,funcname,arg1,...,argN);
其中:
- foo.dll是要导入的动态链接库;
- foo.h是对应的头文件,包含可调用函数的声明;
- funcname是foo.dll中需要调用的函数名称。
需要注意的是,libname中的funcname的参数使用的是C/C++数据类型,而在MATLAB中调用时,实际输入的参数和返回值都是MATLAB数据类型。MATLAB会自动对数据类型进行转换,数据类型的对应关系可参考:。
mex方式
mex是MATLAB提供的一个C/C++/Fortan编译器,可以编译符合一定规范的C/C++/Fortan代码生成MATLAB可调用的二进制文件mexw64(Windows平台下)。
两种方式的对比
loadlibrary |
mex | |
|---|---|---|
| 输入 | libname.dll,libname.h | libname.dll, libname.lib, libname.h, funcName.cxx |
| 输出 | 无 | funcName.mexw64 |
| 语言特性 | 采用 C++ 语言编写的函数必须声明为 extern "C" |
几乎支持全部的C/C++特性 |
使用loadlibrary的主要优点是输入文件比较简单,不需要编写任何额外的代码即可在MATLAB中使用C/C++共享库。缺点则是对C/C++中的自定义struct类型支持较差,自动类型转换往往存在问题(尤其是struct中有嵌套struct数组时)。同时,使用C++代码编写的共享库必须使用extern "C"修饰,这一点也极大限制了C++共享库的使用。
而mex编译器则基于C/C++编译器编译funcName.cxx文件,在编译的过程中利用C/C++编译器链接已有的C/C++库,从而得到MATLAB的可调用函数funcName.mexw64文件。这一方式可以支持C/C++的几乎所有特性(包括函数重载等C++特性)。但代价是用户需要自行编写一个funcName.cxx文件,其中主要需要实现的是完成MATLAB数据类型到C语言数据类型之间的转换。
两种方式的选择
loadlibrary由于限制过多,只能调用简单的C共享库。本文将重点介绍mex方式,关于loadlibrary的使用方法可参考loadlibrary - 将 C 共享库加载到 MATLAB - MATLAB。
显式链接与隐式链接
在开发C/C++代码时,有显式链接与隐式链接两种方法。loadlibrary和mex编译可以认为分别对应了显式链接和隐式链接。二者在程序启动和首次运行时存在一定的差异,但是在连续运行的过程中的性能开销基本一致。但是对于MATLAB-C/C++混合编程来说,使用mex对C++库的限制更少,因为在mexFunction中实际完成了从C/C++到MATLAB的数据类型转换,是对C/C++库进行了一次MATLAB封装。
快速开始
本小节给出了一个使用mex调用C/C++共享库的基本案例,暂时略去背后的原理,仅说明总体的步骤。简单来说分为以下几个步骤:
- 确认mex可用;
- 编写mexFunction;
- 编译生成mexw64文件;
- 在MATLAB中调用mexw64函数。
检查mex是否可用
在MATLAB中使用mex -setup和mex -setup C++可以检查当前计算机中是否有可用的C/C++编译器。
>> mex -setup
MEX configured to use 'Microsoft Visual C++ 2022 (C)' for C language compilation.
To choose a different language, select one from the following:
mex -setup C++
mex -setup FORTRAN
>> mex -setup C++
MEX configured to use 'Microsoft Visual C++ 2022' for C++ language compilation.
MATLAB对不同编译器的支持情况可参考:支持和兼容的编译器。
编写mexFunction
funcName.cxx主要有两个目的:
- 在内部调用C/C++共享库中的函数,得到的计算结果;
- 对函数的输入输出进行处理,实现MATLAB和C/C++之间的数据类型转换。
其基本格式如下:
// fileName: mxAdd.cpp
#include "mex.h"
void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[])
{
// input check
if (nrhs != 2)
{
mexErrMsgIdAndTxt("ZSonic:mxAdd:InvalidNumInputs", "Two input arguments required.");
return;
}
if (nlhs > 1)
{
mexErrMsgIdAndTxt("ZSonic:mxAdd:InvalidNumOutputs", "Too many output arguments.");
return;
}
mwSize ndimA = mxGetNumberOfDimensions(prhs[0]);
const mwSize* dimsA = mxGetDimensions(prhs[0]);
mwSize ndimB = mxGetNumberOfDimensions(prhs[1]);
const mwSize* dimsB = mxGetDimensions(prhs[1]);
if (ndimA != ndimB)
{
mexErrMsgIdAndTxt("ZSonic:mxAdd:DimensionMismatch", "Input dimensions must agree.");
return;
}
for (mwSize i = 0; i < ndimA; ++i)
{
if (dimsA[i] != dimsB[i])
{
mexErrMsgIdAndTxt("ZSonic:mxAdd:DimensionMismatch", "Input dimensions must agree.");
return;
}
}
// create output array
plhs[0] = mxCreateNumericArray(ndimA, dimsA, mxGetClassID(prhs[0]), mxREAL);
double* output = mxGetPr(plhs[0]);
const double* inputA = mxGetPr(prhs[0]);
const double* inputB = mxGetPr(prhs[1]);
for (mwSize i = 0; i < mxGetNumberOfElements(prhs[0]); ++i)
{
output[i] = inputA[i] + inputB[i];
}
}
其中mexFunction是MATLAB调用mexw64函数时的函数入口。函数的四个参数分别为:
nlhs:输出参数的个数;plhs:指向输出参数的指针;nrhs:输入参数的个数;prhs:指向输入参数的指针。
mex.h是MATLAB提供的一个头文件,位置在/path_to_MATLAB/R2024b/extern/include。cxx源代码文件中可以添加其他需要的头文件以实现任意C/C++函数的调用。
MATLAB函数支持可变数量输入输出参数的调用,这在mexFunction中根据nlhs和nrhs的值来实现,函数体中可以加入对这两个参数的值的判断,执行不同的代码。
避免地址越界
在使用prhs和plhs之前,一定要先检查nrhs和nlhs的值,确定参数的数量,避免地址越界。
编译mexFunction
完成funcName.cxx的编写之后,需要通过mex编译生成MATLAB可执行文件mexw64。基本指令如下:
mex funcName.cxx -outdir outDir ...
-IincludePath1 -IincludePath2 ...
-LlibraryPath1 -LlibraryPath2 ...
-llib1.lib -llib2.lib ...
-O -v COMPFLAGS="$COMPFLAGS /std:c++17"
其中:
funcName.cxx是要编译的其中包含mexFunction的源代码文件;-outdir outDir用于指定mexw64文件的输出路径;-IincludePath1用于指定包含路径(和gcc的语法一致),上面提到的/path_to_MATLAB/R2024b/extern/include路径mex会自动添加到包含路径中,不需要用户手动指定;-LlibraryPath1用于指定链接库路径(和gcc的语法一致);-llib1.lib用于指定需要链接的库的名称(和gcc的语法一致);-O用于启用编译器优化;-v用于在编译过程中输出详细的编译信息;COMPFLAGS="$COMPFLAGS /std:c++17"指定使用C++17标准。
Windows与Linux在库命名上的区别
上面的指令在Windows下测试。对于Linux平台,指定链接库的时候有细微的区别。Windows平台下msvc编译器在生成库的时候,会直接在用户指定的库名称的后面加上.lib和.dll后缀,在mex编译时指定要链接的库也需要指定全名。比如库的名称为libfoo.lib,那么需要使用的指令为-llibfoo.lib。而Linux下,gcc编译器在生成库的时候,会固定在用户指定的库名称基础上加上lib前缀和.a后缀(动态链接库则会加上.so后缀)。在指定要链接的库名称时,只给出库的基础名称即可,不能带上lib前缀和.a后缀。
MATLAB数据类型 - mxArray
MATLAB中数据类型的实现
MATLAB内部支持丰富的数据类型,包括:
- 数值类型(single, double, int32, uint32, ...)
- 结构体类型(struct)
- 类(class)
实际上,所有这些数据类型都是对mxArray类型的封装。
mexFunction所实现的功能主要体现在两个方面:
- 实现MATLAB数据类型和C/C++数据类型——尤其是用户自定义数据类型class和struct(MATLAB和C/C++有各自的struct和class的定义与管理方式)——之间的相互转换;
- 实现C/C++库函数的调用,并通过mex编译的方式完成隐式链接。
访问和构造mxArray
在C/C++代码中可以通过一系列API获取mxArray的属性和其中所保存的数据。
mwSize ndim = 3;
mwSize dims[3];
dims[0] = 16;
dims[1] = 8;
dims[2] = 4;
// 创建一个数值类型的mxArray
mxArray *array = mxCreateNumericArray(ndim, dims, mxSINGLE_CLASS, mxREAL);
// 获取数据指针
float* data = static_cast<float*>(mxGetData(array));
uint32_t numberOfElements = mxGetNumberOfElements(array);
// 处理数据
for (uint32_t i = 0; i < numberOfElements; ++i)
{
data[i] = i;
}
结构体类型的mxArray
除了数值类型的mxArray(对应MATLAB中的普通矩阵类型),还有一种结构体类型的mxArray。
mwSize ndim;
mwSize dims[2];
dims[0] = 1;
dims[1] = 8;
mwSize numFields = 4;
const char* fieldsName[] = {
"Name",
"Age",
"Gender",
"Score"
};
mxArray *structArray = mxCreateStructArray(ndim, dims, numFields, fieldsName);
上面的代码会创建一个 \(1\times 8\) 的结构体数组。结构体包含4个field,名称由fieldsName中的字符串指定。