跳转至

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++共享库的基本案例,暂时略去背后的原理,仅说明总体的步骤。简单来说分为以下几个步骤:

  1. 确认mex可用;
  2. 编写mexFunction;
  3. 编译生成mexw64文件;
  4. 在MATLAB中调用mexw64函数。

检查mex是否可用

在MATLAB中使用mex -setupmex -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中根据nlhsnrhs的值来实现,函数体中可以加入对这两个参数的值的判断,执行不同的代码。

避免地址越界

在使用prhsplhs之前,一定要先检查nrhsnlhs的值,确定参数的数量,避免地址越界。

编译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所实现的功能主要体现在两个方面:

  1. 实现MATLAB数据类型和C/C++数据类型——尤其是用户自定义数据类型class和struct(MATLAB和C/C++有各自的struct和class的定义与管理方式)——之间的相互转换;
  2. 实现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中的字符串指定。

参考资料

  1. mex - 编译 MEX 函数和引擎或 MAT 文件应用程序