☰
C++调用MATLAB实战:Engine、Compiler SDK与Coder选型指南
2026/10/12 21:57:03 网站建设 项目流程

1. 为什么要在C++里调用MATLAB

先把场景说清楚。你手上有一个用C++写的主程序,可能是某个仿真系统、某个数据处理工具,或者某个需要长期稳定运行的服务端程序。同时,团队里有人用MATLAB把算法原型跑通了,里面有大量的矩阵运算、信号处理函数、工具箱调用,甚至还有Simulink生成的模型。现在的问题是:怎么把这两部分接起来?

最直接的想法是用C++把MATLAB的算法重写一遍。但实际操作过的人都知道,这件事的代价极高。MATLAB里一行fft(x),你用C++手写可能要调FFTW、处理复数数组、管理内存对齐,还得验证数值精度是否一致。更不用说那些工具箱函数,比如滤波器设计、优化求解、统计分布拟合,重写一遍基本等于重新做一个项目。

所以,C++调用MATLAB的核心价值就一句话:复用已有的MATLAB算法资产,同时保留C++主程序的性能和工程结构。这不是为了炫技,而是工程上最省时省力的路径。

适合看这篇总结的人,大概分三类。第一类是做工程软件开发的,主程序是C++,但算法团队交付的是.m文件或.mlx文件。第二类是做硬件在环测试的,C++负责实时调度,MATLAB负责模型运算。第三类是学生或研究者,想把MATLAB验证过的算法嵌入到一个C++项目里做演示或产品化。不管哪一类,核心诉求都一样:让两个世界的数据和函数能互相调用,而且尽量少踩坑。

我前后做过四五个这类项目,从最早的MATLAB Engine方式,到后来的MATLAB Compiler SDK生成库,再到用MATLAB Coder直接生成C++代码,每条路都走过。下面把经验按方案选型、环境配置、实操步骤、问题排查几个维度拆开讲,尽量把每个选择的理由和背后的坑都说清楚。

2. 三条主流路线怎么选

2.1 MATLAB Engine:适合调试和原型验证

MATLAB Engine的本质是让C++程序启动一个MATLAB进程,通过进程间通信来执行MATLAB命令。你可以把它理解成“在C++里开了一个MATLAB的命令行窗口”,只不过这个窗口是通过API来控制的。

它的优点很明确:不需要编译MATLAB代码,不需要额外的工具箱授权(除了MATLAB本身),支持几乎所有MATLAB函数,包括那些无法用Coder生成的函数。调试阶段特别方便,因为你可以直接在C++里执行eval字符串,看到MATLAB那边的变量和输出。

但缺点同样明显。首先,它依赖本机安装的MATLAB,部署时必须确保目标机器有MATLAB和对应的授权。其次,每次调用都有进程间通信的开销,单次调用大概在毫秒级别,如果算法本身很快,这个开销占比就很大。第三,它不适合多线程并发调用,Engine的API在并发场景下需要加锁,否则容易出问题。

我一般把Engine方式用在两个场景:一是算法还在频繁迭代,需要快速验证;二是最终产品不需要独立部署,运行环境里本来就有MATLAB。

2.2 MATLAB Compiler SDK:适合独立部署

Compiler SDK的思路是把MATLAB函数编译成C/C++共享库,然后C++程序像调用普通动态库一样调用它。编译出来的库依赖MATLAB Runtime,这是一个免费的运行时环境,不需要MATLAB授权。

这条路线的核心优势是部署干净。目标机器只需要装MATLAB Runtime,不需要装完整的MATLAB。调用开销也比Engine小很多,因为不涉及进程间通信,是直接的函数调用。

但限制也很清楚。首先,不是所有MATLAB函数都支持编译,比如某些符号计算、某些工具箱的特定函数,编译时会直接报错。其次,每次修改MATLAB代码都需要重新编译生成库,迭代速度比Engine慢。第三,MATLAB Runtime的版本必须和编译时使用的MATLAB版本匹配,版本管理要小心。

我通常在产品化阶段选这条路,尤其是需要把算法交付给第三方、又不想暴露源码的场景。

2.3 MATLAB Coder:适合性能敏感场景

MATLAB Coder直接把MATLAB代码转换成C/C++源码。你拿到的是纯C++代码,不依赖MATLAB Runtime,可以嵌入到任何C++项目里,性能也最好。

但它的限制最多。首先,不是所有MATLAB代码都能转换,必须满足Coder的子集要求,比如不支持动态类型、不支持某些高级数据结构。其次,转换后的代码可读性一般,调试起来比较麻烦。第三,需要MATLAB Coder的授权,不是所有MATLAB版本都包含。

我一般只在算法固定、性能要求高、且不需要频繁修改的场景下用Coder。比如某个已经验证稳定的滤波算法,要嵌入到实时系统里,这时候Coder生成的代码最合适。

2.4 三条路线的对比

对比维度MATLAB EngineCompiler SDKMATLAB Coder
部署依赖需要完整MATLAB需要MATLAB Runtime无依赖
调用开销毫秒级微秒级纳秒级
支持函数范围几乎全部大部分子集
迭代速度快中慢
授权要求MATLABMATLAB + Compiler SDKMATLAB + Coder
适合阶段调试/原型产品化性能敏感

选哪条路,核心看三个问题:目标机器能不能装MATLAB?算法会不会频繁改?性能要求有多高?把这三点想清楚,选择就不难了。

3. 环境配置与版本匹配的坑

3.1 版本兼容性是第一道坎

MATLAB和C++编译器的版本匹配,是这类项目里最容易出问题的地方。MATLAB每个版本支持的编译器列表是固定的,比如某个MATLAB版本只支持特定版本的Visual Studio或GCC。如果你用的编译器不在支持列表里,mex编译会直接失败,或者编译出来的库在运行时报错。

我的做法是:先确定MATLAB版本,然后去查该版本支持的编译器列表,再安装对应的编译器。不要反过来,先装最新编译器再配MATLAB,那样大概率会踩坑。

另外,MATLAB Runtime的版本必须和编译时使用的MATLAB版本一致。比如你用某个版本的MATLAB编译了库,目标机器上就必须装对应版本的Runtime。Runtime不能跨版本兼容,这一点在部署文档里要写清楚。

3.2 环境变量与路径配置

Engine方式需要C++程序能找到MATLAB的安装路径。通常需要设置PATH环境变量,把MATLAB的bin目录加进去。在Windows上,还需要确保MATLABROOT环境变量指向正确的安装目录。

Compiler SDK方式需要配置库路径和头文件路径。编译时用mcc命令生成库,然后把生成的.h、.lib、.dll文件放到C++项目里。注意,生成的库文件名和函数名可能和MATLAB函数名不完全一致,需要看生成的文档。

Coder方式生成的是源码,直接加入C++项目即可。但要注意生成代码的依赖项,有些情况下需要手动添加额外的源文件。

3.3 一个容易被忽略的细节:字符编码

MATLAB的字符串默认是UTF-16,而C++的std::string通常是UTF-8或本地编码。在传递字符串参数时,如果编码不一致,会出现乱码或函数调用失败。我的经验是,在C++侧统一用mwString或mwArray的字符串接口,避免直接传char*。如果必须传,确保在边界处做编码转换。

4. Engine方式的实操步骤

4.1 初始化与启动

Engine方式的核心类是matlab::engine::MATLABEngine。在C++里启动MATLAB的典型代码如下:

#include "MatlabEngine.hpp" #include "MatlabDataArray.hpp" using namespace matlab::engine; int main() { // 启动MATLAB引擎 std::unique_ptr<MATLABEngine> matlabPtr = startMATLAB(); // 执行MATLAB命令 matlabPtr->eval(u"disp('Hello from MATLAB')"); return 0; }

这段代码看起来简单,但有几个细节要注意。startMATLAB()会启动一个新的MATLAB进程,这个过程大概需要几秒钟。如果程序需要频繁启动和关闭,建议在程序初始化时启动一次,然后复用这个matlabPtr。

另外,eval的参数是std::u16string,不是普通的std::string。这是因为MATLAB内部用UTF-16。如果你从配置文件读取命令,需要先做编码转换。

4.2 数据交换:mwArray与matlab::data

Engine方式的数据交换有两种接口:旧的mwArray和新的matlab::data::Array。新接口更类型安全,推荐使用。

创建一个MATLAB数组并传给引擎:

#include "MatlabDataArray.hpp" using namespace matlab::data; // 创建一个2x3的double矩阵 ArrayFactory factory; TypedArray<double> matrix = factory.createArray<double>({2, 3}, { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0 }); // 把矩阵放到MATLAB工作区 matlabPtr->setVariable(u"inputMatrix", matrix); // 在MATLAB里调用函数 matlabPtr->eval(u"resultMatrix = myFunction(inputMatrix);"); // 取回结果 TypedArray<double> result = matlabPtr->getVariable(u"resultMatrix");

这里的关键是ArrayFactory和TypedArray。ArrayFactory负责创建各种类型的数组,TypedArray是模板类,支持double、float、int等类型。取回数据时,可以用result.getDimensions()获取维度,用result[i]或迭代器访问元素。

注意:getVariable返回的是数组的副本,如果数据量很大,会有内存拷贝开销。对于大矩阵,可以考虑用matlab::data::Array的引用语义,或者直接在MATLAB侧处理完再取回小结果。

4.3 调用MATLAB函数并传参

假设你有一个MATLAB函数myFilter.m,定义如下:

function y = myFilter(x, fs, cutoff) [b, a] = butter(4, cutoff/(fs/2)); y = filtfilt(b, a, x); end

在C++里调用它:

// 准备输入参数 TypedArray<double> x = factory.createArray<double>({1000, 1}, signalData); TypedArray<double> fs = factory.createScalar(1000.0); TypedArray<double> cutoff = factory.createScalar(50.0); // 调用函数 std::vector<Array> args = {x, fs, cutoff}; std::vector<Array> results = matlabPtr->feval(u"myFilter", args); // 取回结果 TypedArray<double> y = results[0];

feval是调用MATLAB函数的核心接口。第一个参数是函数名,第二个参数是参数列表。返回值是一个std::vector<Array>,因为MATLAB函数可能返回多个值。

这里有个细节:feval的函数名不需要加.m后缀,但函数必须在MATLAB的搜索路径里。如果函数在某个特定目录下,需要先用addpath把目录加进去。

4.4 异常处理

Engine的API会抛出matlab::engine::MATLABException异常。如果MATLAB函数执行出错,比如参数类型不对、函数不存在,都会抛异常。所以调用feval和eval时,建议用try-catch包起来:

try { matlabPtr->eval(u"result = myFunction(input);"); } catch (const matlab::engine::MATLABException& e) { std::cerr << "MATLAB error: " << e.what() << std::endl; }

e.what()返回的是UTF-16字符串,需要转换成std::string才能输出。可以用matlab::engine::convertUTF16StringToUTF8String做转换。

5. Compiler SDK方式的实操步骤

5.1 编译MATLAB函数为库

Compiler SDK的核心命令是mcc。假设你要把myFilter.m编译成C++共享库:

mcc -W cpplib:libmyFilter -T link:lib myFilter.m

这条命令会生成libmyFilter.h、libmyFilter.lib、libmyFilter.dll等文件。-W cpplib:指定生成C++库,-T link:lib指定生成链接库。

如果MATLAB函数有多个文件,比如myFilter.m依赖helper.m,需要把依赖文件也列出来:

mcc -W cpplib:libmyFilter -T link:lib myFilter.m helper.m

编译过程中,MATLAB会分析依赖关系,把需要的函数打包进去。但有些工具箱函数可能无法打包,编译时会报错。这时候需要检查函数是否在支持列表里。

5.2 在C++中调用生成的库

生成的库使用mwArray作为数据接口。典型的调用代码如下:

#include "libmyFilter.h" int main() { // 初始化MATLAB Runtime if (!libmyFilterInitialize()) { std::cerr << "Failed to initialize library" << std::endl; return -1; } // 准备输入数据 double signalData[1000] = { /* ... */ }; mwArray x(1000, 1, mxDOUBLE_CLASS); x.SetData(signalData, 1000); mwArray fs(1, 1, mxDOUBLE_CLASS); fs(1, 1) = 1000.0; mwArray cutoff(1, 1, mxDOUBLE_CLASS); cutoff(1, 1) = 50.0; // 调用函数 mwArray y; myFilter(1, y, x, fs, cutoff); // 取回结果 double* resultData = new double[1000]; y.GetData(resultData, 1000); // 清理 delete[] resultData; libmyFilterTerminate(); return 0; }

这里有几个关键点。第一,libmyFilterInitialize()和libmyFilterTerminate()必须成对调用,前者初始化Runtime,后者释放资源。第二,mwArray的构造函数参数是行数、列数和类型。第三,myFilter函数的第一个参数是输出参数个数,第二个是输出数组,后面是输入参数。

注意:mwArray的内存管理是引用计数,但GetData返回的是原始指针,需要手动管理内存。如果数据量很大,建议用mwArray的GetData重载版本,直接传入预分配的缓冲区。

5.3 部署时的Runtime配置

目标机器需要安装MATLAB Runtime。Runtime的安装包可以从MATLAB安装目录里找到,或者用mcrinstaller命令生成。安装后,需要把Runtime的bin目录加到PATH里,否则程序启动时会找不到依赖库。

另外,Runtime的版本必须和编译时使用的MATLAB版本一致。如果目标机器上装了多个版本的Runtime,需要确保程序加载的是正确版本。在Windows上,可以用SetDllDirectory指定Runtime的路径。

6. MATLAB Coder方式的实操步骤

6.1 生成C++代码

MATLAB Coder的使用方式是在MATLAB里配置一个Coder项目,然后生成代码。核心步骤是:

% 创建Coder配置 cfg = coder.config('lib'); cfg.TargetLang = 'C++'; % 指定输入参数类型 args = {coder.typeof(0, [1000, 1]), coder.typeof(0), coder.typeof(0)}; % 生成代码 codegen -config cfg myFilter -args args -report

codegen命令会生成一个codegen目录,里面有myFilter.cpp、myFilter.h等文件。-report选项会生成一个报告,显示哪些函数被转换了,哪些没有。

生成的代码是纯C++,不依赖MATLAB Runtime。但要注意,生成的代码可能包含一些MATLAB特有的数据结构,比如emxArray_real_T,这是Coder用来表示动态数组的。在C++侧调用时,需要按这些结构来传参。

6.2 集成生成的代码

生成的代码通常包含一个入口函数,比如myFilter。调用方式如下:

#include "myFilter.h" int main() { // 准备输入数据 double signalData[1000] = { /* ... */ }; emxArray_real_T* x = emxCreateWrapper_real_T(signalData, 1000, 1); double fs = 1000.0; double cutoff = 50.0; // 准备输出 emxArray_real_T* y = emxCreate_real_T(1000, 1); // 调用函数 myFilter(x, fs, cutoff, y); // 使用结果 for (int i = 0; i < y->size[0]; i++) { std::cout << y->data[i] << std::endl; } // 清理 emxDestroyArray_real_T(x); emxDestroyArray_real_T(y); return 0; }

emxArray_real_T是Coder生成的动态数组类型。emxCreateWrapper_real_T创建一个包装现有数据的数组,不拷贝数据。emxCreate_real_T创建一个新的数组,会分配内存。使用完后必须用emxDestroyArray_real_T释放。

注意:Coder生成的代码默认使用emxArray,但如果输入输出是固定大小的,可以在Coder配置里关闭动态内存分配,生成更简单的接口。在coder.config里设置cfg.DynamicMemoryAllocation = 'Off'即可。

6.3 性能调优

Coder生成的代码性能通常比手写的C++差一些,因为要处理MATLAB的语义,比如边界检查、动态类型等。如果性能不够,可以尝试以下优化:

  • 关闭运行时检查:cfg.RuntimeChecks = false
  • 开启编译器优化:在C++项目里用-O2或-O3
  • 使用固定大小数组:避免动态内存分配
  • 内联小函数:cfg.InlineBetweenUserFunctions = 'Always'

这些优化会牺牲一些安全性,但能显著提升性能。我实测过一个滤波算法,关闭运行时检查后,速度提升了大概30%。

7. 常见问题与排查技巧

7.1 编译报错:找不到头文件

这是最常见的问题。Engine方式需要包含MatlabEngine.hpp和MatlabDataArray.hpp,这两个文件在MATLAB安装目录的extern/include下。Compiler SDK方式需要包含生成的.h文件,通常在mcc输出目录里。

排查步骤:

  1. 确认MATLAB安装路径是否正确
  2. 检查C++项目的包含目录是否加了extern/include
  3. 检查库目录是否加了extern/lib和bin/win64(Windows)或bin/glnxa64(Linux)

7.2 运行时错误:找不到DLL

Windows上最常见的是libmx.dll、libmat.dll、libeng.dll找不到。这些DLL在MATLAB的bin/win64目录下。解决方法有两种:一是把该目录加到PATH环境变量;二是把DLL拷贝到可执行文件同目录。

Compiler SDK方式还需要mclmcrrt*.dll,这些在MATLAB Runtime的bin/win64目录下。

7.3 数据不一致:矩阵维度对不上

MATLAB的矩阵是列优先存储,C++的数组通常是行优先。在传递多维数组时,如果维度顺序搞错,结果会完全不对。我的经验是:在C++侧创建mwArray或TypedArray时,明确指定行数和列数,不要依赖默认行为。取回数据时,先用getDimensions()确认维度,再访问元素。

7.4 性能问题:调用开销太大

Engine方式的单次调用开销在毫秒级,如果算法本身很快,这个开销占比就很大。优化方法:

  • 批量处理:一次调用处理多个数据,而不是循环调用
  • 减少数据拷贝:用引用语义传递大数组
  • 考虑改用Compiler SDK或Coder

Compiler SDK的调用开销在微秒级,Coder在纳秒级。如果性能要求高,优先考虑后两者。

7.5 版本冲突:多个MATLAB版本共存

如果机器上装了多个MATLAB版本,环境变量可能指向错误的版本。排查方法:

  • 检查PATH里MATLAB的bin目录顺序
  • 检查MATLABROOT环境变量
  • 在代码里打印MATLAB版本:matlabPtr->eval(u"disp(version)")

7.6 常见问题速查表

问题现象可能原因解决方法
编译时报找不到头文件包含路径未配置添加MATLAB的extern/include目录
运行时找不到DLL库路径未配置添加MATLAB的bin目录到PATH
矩阵结果不对维度顺序错误确认行优先/列优先,检查维度参数
调用速度慢Engine通信开销批量处理或改用Compiler SDK
版本不匹配多版本共存检查PATH和MATLABROOT
字符串乱码编码不一致统一用UTF-16或做编码转换

8. 我踩过的几个坑和对应技巧

第一个坑是在循环里反复调用Engine。早期做一个数据处理项目,我在C++循环里逐条调用MATLAB函数,结果速度慢得无法接受。后来改成把数据攒成一批,一次传给MATLAB处理,速度提升了两个数量级。这个经验告诉我:Engine方式一定要批量处理,不要频繁跨进程通信。

第二个坑是忽略MATLAB函数的路径依赖。有一次feval一直报函数不存在,查了半天才发现函数在某个子目录里,而MATLAB的当前工作目录不对。后来养成习惯:在C++程序启动时,先用eval执行addpath,把所有需要的目录加进去。

第三个坑是Compiler SDK编译时漏了依赖文件。MATLAB函数如果调用了其他.m文件,mcc命令必须把这些文件都列出来,否则运行时会报函数未定义。更隐蔽的是,有些依赖是动态的,比如通过str2func调用的函数,mcc分析不出来,需要手动用-a选项添加。

第四个坑是Coder生成的代码在Release模式下崩溃。Debug模式下正常,Release模式下崩溃,最后发现是Coder生成的代码里有未初始化的变量,Debug模式下恰好是0,Release模式下是随机值。解决方法是在Coder配置里开启cfg.RuntimeChecks,或者在C++侧确保所有输入都初始化。

第五个坑是MATLAB Runtime版本不匹配。有一次在目标机器上装了新版本的Runtime,结果程序启动时报错。后来查文档才知道,Runtime必须和编译时的MATLAB版本完全一致,不能跨版本使用。现在我在部署文档里会明确写清楚Runtime的版本号。

最后分享一个实用技巧:用MATLAB的diary功能记录Engine的调用日志。在C++程序里执行eval(u"diary('engine_log.txt')"),MATLAB会把所有命令和输出写到日志文件里。排查问题时,这个日志比C++侧的异常信息有用得多。

另一个技巧是用mwArray的ToString方法快速查看数据。调试时,把mwArray转成字符串打印出来,比逐个元素访问快得多。虽然性能不好,但调试阶段非常方便。

还有一个经验:尽量把MATLAB代码写成纯函数,不要依赖全局变量或持久变量。这样无论是Engine、Compiler SDK还是Coder,行为都更可预测。全局变量在Engine方式下可能正常工作,但在Coder方式下可能直接报错。

关于性能优化,我的建议是先测量再优化。用tic和toc在MATLAB侧测量算法本身的耗时,用C++的计时器测量调用开销。如果算法耗时远大于调用开销,那优化调用方式意义不大;如果调用开销占比高,再考虑换方案或批量处理。

最后一点:版本控制要包含MATLAB代码和生成的库。MATLAB代码用Git管理,生成的库文件(.dll、.lib、.h)也建议纳入版本控制,或者至少记录生成时的MATLAB版本和mcc命令。否则过几个月再想重新生成,可能因为环境变化而失败。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询