ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

VS与Qt调用Matlab函数:从MATLAB Compiler SDK到C++工程集成全指南

VS与Qt调用Matlab函数:从MATLAB Compiler SDK到C++工程集成全指南 VS(QT)调用Matlab函数的方法这个话题我前前后后折腾了差不多两周才彻底跑通。最开始只是在VS里写个小工具后来项目迁到了Qt又踩了一轮坑。今天把这些经验整理出来从思路选型到环境配置从编译生成到工程集成再到那些文档里根本不会写的坑一次性说清楚。先说清楚这个东西到底解决什么问题、适合谁看。如果你手里有一堆已经写好的Matlab算法比如信号处理、图像处理、曲线拟合、数值优化这类不想用C重写一遍而是想在VS或者Qt的C工程里直接调用这些Matlab函数这篇文章就是给你写的。适合刚接触混合编程的C开发者也适合对Matlab比较熟但不太清楚怎么把m文件转成C接口的人。1. 为什么要在VS/Qt里调用Matlab函数1.1 这个需求从哪来实际项目里经常碰这种情况算法团队用Matlab开发出了一套很复杂的计算逻辑跑出来的结果没问题但交付给工程团队后工程团队要把这套算法集成到自己的C桌面应用或者嵌入到某个上位机系统里。完整用C重写一遍一来算法可能非常复杂比如有很多矩阵运算、迭代求解、高度依赖Matlab工具箱二来验证正确性也要花大量时间项目周期根本不允许。另一个常见场景是你需要在C程序里验证某个Matlab算法的中间结果或者需要把C处理完的数据丢给Matlab后处理再拿回结果。这种情况下没必要把Matlab代码翻译成C直接在C里调用Matlab函数能省非常多的工作量。这里说的“调用”不是开一个Matlab进程然后通过命令行传参那样太笨太重。我们说的是把Matlab函数直接编译成DLLWindows环境下的动态链接库让VS或者Qt的C程序像调用普通C库一样去调用Matlab函数。数据在内存里直接传输不需要经过文件落地效率高得多。1.2 几条技术路线的对比我在做方案选型的时候其实对比过几条路线完整把Matlab代码用C重写优点完全独立部署不依赖Matlab运行时。 缺点开发周期长算法越复杂越容易出问题还要维护两套代码的一致性。 适合代码简单、性能要求苛刻、部署环境不允许装任何Matlab组件的场景。通过MATLAB Engine API远程驱动Matlab计算利用Matlab自带的Engine库在C程序里启动Matlab进程然后动态调用函数。 优点可以在C里直接使用Matlab工作空间变量调试方便。 缺点目标机器必须安装完整版Matlab启动慢内存占用大部署成本高。说白了就适合自己开发调试用不适合交付。用MATLAB Compiler SDK把m函数编译成C共享库也就是这次要重点讲的方式。 优点生成的DLL及必要的运行时库可以在没有完整Matlab的机器上运行只要装一个Matlab RuntimeMCR或MWR即可。运行时体积比完整Matlab小很多启动速度快。代码以C类接口暴露常规C工程集成很容易。 缺点编译过程有一定门槛mwArray等Matlab数据类型需要适应函数签名设计如果没规划好后面会比较难受。我最后选的就是第三条路线。因为我的使用场景是做工具软件既要给别的同事直接用又不想每个同事的电脑上都装完整Matlab。提示如果你预算充足或者只是个人使用装完整Matlab然后走Engine路线确实调试更直观。但一旦涉及交付、部署、给第三方用老老实实学Compiler SDK更划算也更有工程价值。2. 环境准备与前置配置2.1 需要的软件版本我开发时候的环境是Windows 1064位、Matlab R2020b64位、Visual Studio 2019、Qt 5.14.2MSVC2017_64套件。有几个版本相关的坑必须先提一下MATLAB Compiler SDK在R2015a之后就合并到了MATLAB Compiler工具箱里不再需要额外安装单独的工具箱只要你的Matlab许可协议里包含Compiler功能就行。用ver命令查一下如果有“MATLAB Compiler”和“MATLAB Compiler SDK”这两个条目就OK。VS版本和Matlab版本是有兼容要求的。Matlab R2020b官方支持VS2019但老的Matlab比如R2016a只支持VS2013你拿新版VS去编译生成的C库虽然有时候能侥幸通过但遇到奇怪的链接错误别惊讶。建议先查好官方支持矩阵再动手。下面这个表是我当时整理的常用搭配参考Matlab版本官方支持的VS版本Windows备注R2018aVS2015、VS2017较新版本指令集兼容性问题较少R2019bVS2017、VS2019比较稳定的组合R2020bVS2019本文实测环境R2022aVS2019、VS2022新版建议直接用VS2022Qt这边必须使用MSVC编译套件不能使用MinGW。因为Matlab生成的库是Microsoft格式的DLL和libMinGW链接不上会报一堆未定义符号。这个属于刚入坑时最容易踩的问题我有个同事就是卡在这里好几天没想明白。2.2 配置Matlab的C编译器在Matlab命令行里跑mex -setup C如果之前没配置过会列出一堆已安装的编译器。选择你要用的VS版本比如Microsoft Visual C 2019。这一步本质上是让Matlab知道它在接下来准备C头文件和库的路径时按MSVC的规则来。然后用Matlab自带的部署工具来验证环境是否正常在Command Window执行deploytool弹出窗口中选择Library Compiler。如果环境正常会直接进入打包界面。如果报错说找不到编译器多半是VS版本不匹配或者VS没装C桌面开发组件就是那个“使用C的桌面开发”Workload。重新打开VS Installer装上那个组件再回来跑一次mex -setup就好了。另外还得注意系统环境变量。Matlab安装完成后MATLAB_ROOT比如D:\Program Files\MATLAB\R2020b下面的路径一般会自动写入系统PATH。但如果你电脑原来装过老版本Matlab新老版本路径都留在PATH里优先用老版本的话编译出来的库调用时可能连到错误版本的dll导致运行时崩溃。建议进“环境变量-系统变量-Path”把不需要的那个版本相关的路径删掉只保留当前用的版本。2.3 检查磁盘空间和权限别小看这一步。Matlab生成C库时会在临时目录释放一堆中间文件另外最终运行时除了DLL本身还要依赖Matlab Runtime文件。如果选“没有完整Matlab的目标机器上也能运行”的部署模式运行时包大概有几个GB。如果只在这个开发机上跑就无所谓直接依赖本机Matlab运行时也行生成库时选择当前机器使用。实操时建议预留至少5GB空间最好在D盘。C盘太满的话Matlab编译过程会报临时目录空间不足这种错误提示又很隐晦我遇到过两次才反应过来。3. 用MATLAB Compiler SDK生成C共享库3.1 先写一个可被编译的m函数假设我们要把这样一个简单但典型的Matlab函数编译成库输入一组x和y序列做多项式拟合返回拟合系数和预测值。这个函数在信号处理、数据标定里非常常用包含数值计算、返回多个输出参数拿来演示刚好。先创建一个函数文件polyfit_demo.mfunction [coeffs, yfit] polyfit_demo(x, y, n) % 多项式拟合demo函数 % 输入: x, y 为列向量数据n 为拟合阶数 % 输出: coeffs 为多项式系数按降幂排列yfit 为对应x的拟合值 coeffs polyfit(x, y, n); yfit polyval(coeffs, x); end这个文件放的位置没有严格要求但建议放在一个干净的目录下比如D:\matlab_export\src因为打包的时候会把当前目录下的依赖文件一起分析进去太乱容易把不相关的文件也打进包里。有一点要注意被编译的m函数不允许存在脚本式全局变量的依赖。也就是说函数内部不能依赖base工作空间的变量所有输入输出都必须通过参数传递。如果你的算法是项目里之前那种依赖Matlab脚本环境比如从workspace里直接取变量写出来的编译前必须先把它重构为纯函数。3.2 使用deploytool图形界面打包在写好的m文件目录下命令行执行deploytool然后左侧选择Library Compiler在“Export Functions”里添加polyfit_demo.m在“Library Name”里填一个库名比如PolyfitLib。“Packaging Options”里如果目标机器不确定可以选择“Runtime downloaded from web”这样目标机器安装运行时包时从网上下载如果要求离线部署就选“Runtime included in package”右上角“Type”下拉选择C Shared Library填一个Company和Version然后点“Package”。打包过程会持续几分钟到十几分钟不等取决于函数复杂度和机器性能。结束后在指定输出目录里会生成很多文件夹其中最重要的是三个PolyfitLib.hC头文件声明了我们的Matlab函数的C包装类。PolyfitLib.lib链接用的静态导入库。PolyfitLib.dll动态链接库本体。for_redistribution_files_only目录存放上面这几个核心文件目标机器部署时用这个目录的内容就够。for_testing目录包含测试用的可执行程序和使用说明适合开发调试阶段使用。3.3 命令行方式打包如果你是那种不爱点图形界面的类型也可以直接命令行打包。原理和上面完全一样mcc -W cpplib:PolyfitLib -T link:lib polyfit_demo.m参数说明-W cpplib:PolyfitLib指定生成C库库名是PolyfitLib-T link:lib生成动态链接库而不是可执行文件后面跟m文件名如果函数依赖其他m文件一并写上。命令行方式的好处是可复现。把这条命令写到一个.bat或者Matlab脚本里以后改了函数再生成一条命令搞定不容易漏点。图形界面有时候选项多了会忘记哪个没选。3.4 生成的C头文件长什么样打包完成后打开PolyfitLib.h你会看到类似这样的结构R2020b生成版本不同会有细微差异class LIB_libPolyfitLib_CPP_API PolyfitLib { public: PolyfitLib(); ~PolyfitLib(); void polyfit_demo(int nargout, mwArray coeffs, mwArray yfit, const mwArray x, const mwArray y, const mwArray n); };这里nargout表示期望的输出参数个数后面跟着的是输出参数引用然后是输入参数。m文件里的coeffs, yfit变成了第一个输出引用传递x, y, n变成了输入。关键数据类型是mwArray它负责把C的各种数值数据转成Matlab能识别的数组格式。后面所有数据交互都要靠它这是整个集成过程里最核心的接口概念。3.5 需要的运行时依赖编译产物本身不大但它依赖Matlab Runtime库。如果本机安装的是完整Matlab运行时库就在MATLAB_ROOT\runtime\win64下。如果目标机器上只有Matlab Runtime可以从Matlab官网下载对应版本单独安装则运行时库在Program Files\MATLAB\MATLAB Runtime\R2020b\runtime\win64。我们需要把运行时库里的bin目录加入PATH否则调用DLL时Windows找不到依赖项。实际部署时最稳妥的做法是把runtime\win64里的所有dll全部拷到和生成的PolyfitLib.dll同一个目录下。虽然粗暴了一点但亲测有效不用单独配置全局PATH也不会因为PATH优先级问题链接到错误版本。注意Matlab Runtime版本必须和编译时使用的Matlab版本完全一致或兼容小版本。R2020b编译的库放到只有R2021a运行时的机器上初期可能会正常但概率性崩溃这个就别赌了老老实实装R2020b的Runtime。4. 在VS工程里集成生成的库4.1 新建一个干净的C工程我用的是VS2019新建一个“控制台应用C”。工程创建好之后先别急着写代码先做工程配置。把编译生成的三个核心文件PolyfitLib.h、PolyfitLib.lib、PolyfitLib.dll放到一个固定目录比如D:\my_project\3rdparty\PolyfitLib\include\PolyfitLib.h D:\my_project\3rdparty\PolyfitLib\lib\PolyfitLib.lib D:\my_project\3rdparty\PolyfitLib\bin\PolyfitLib.dll这样以后整个工程打包、拷贝给别人不会乱。直接塞到VS工程目录里当然也行但多人协作时还是独立第三方目录更规范。4.2 工程配置的几个关键入口VS的工程配置都在“项目-属性”里C/C - 常规 - 附加包含目录添加三处PolyfitLib.h所在目录Matlab根目录下的extern\include比如D:\Program Files\MATLAB\R2020b\extern\includeMatlab根目录下的extern\include\win64里面还有一层子目录需要引用。为什么要有Matlab的include目录因为PolyfitLib.h里面会includemclcppclass.h和matrix.h这些Matlab运行时头文件它们并不随生成的库分发还是依赖本机Matlab安装目录。如果目标机器只装了Runtime那include路径就要指向Runtime安装目录下对应的地方MATLAB Runtime\R2020b\extern\include和...\extern\include\win64。链接器 - 常规 - 附加库目录添加PolyfitLib.lib所在目录Matlab根目录下的extern\lib\win64\microsoft注意这个目录里有debug和release两个子目录包括mclmcrrt.lib等核心库如果还想用其他Matlab运行时函数再添extern\lib\win64也可以。链接器 - 输入 - 附加依赖项添加PolyfitLib.lib mclmcrrt.lib libmat.lib libmx.libmclmcrrt.lib是Matlab运行时核心库必加。libmat和libmx用于矩阵和数据容器操作。如果编译报缺哪个符号再回来查是不是漏了某个lib。C/C - 代码生成 - 运行库这里有个非常隐蔽的坑。Debug配置必须选“多线程调试(/MTd)”Release配置必须选“多线程(/MT)”。如果选了“/MDd”或“/MD”链接时会报一堆LIBCMT和LIBCMTD冲突的错误因为Matlab运行时是用静态运行库编译的和我们工程里的动态运行库对不上。这个配置错了错误信息满屏飞因此要提前设置好免得以为自己代码写错了。4.3 写一个调用demo配置好之后写一个最简单的测试程序把数据传进去拿回拟合结果#include iostream #include PolyfitLib.h int main() { // 初始化Matlab运行时必须最先执行 if (!mclInitializeApplication(NULL, 0)) { std::cerr mclInitializeApplication failed std::endl; return -1; } try { PolyfitLib lib; // 创建库实例 // 准备数据 double xData[] { 0.0, 1.0, 2.0, 3.0, 4.0, 5.0 }; double yData[] { 1.0, 1.8, 3.2, 6.0, 10.2, 15.8 }; int n 2; // 数据转成mwArray mwArray x(6, 1, mxDOUBLE_CLASS); mwArray y(6, 1, mxDOUBLE_CLASS); x.SetData(xData, 6); y.SetData(yData, 6); mwArray order(1, 1, mxDOUBLE_CLASS); order.SetData(n, 1); // 输出数组 mwArray coeffs; mwArray yfit; // 调用Matlab函数 int nargout 2; lib.polyfit_demo(nargout, coeffs, yfit, x, y, order); // 提取结果 int rows coeffs.NumberOfElements(); double* pCoeffs new double[rows]; coeffs.GetData(pCoeffs, rows); int fitRows yfit.NumberOfElements(); double* pYfit new double[fitRows]; yfit.GetData(pYfit, fitRows); std::cout coeffs:; for (int i 0; i rows; i) { std::cout pCoeffs[i]; } std::cout std::endl; std::cout yfit:; for (int i 0; i fitRows; i) { std::cout pYfit[i]; } std::cout std::endl; delete[] pCoeffs; delete[] pYfit; } catch (const mwException e) { std::cerr Matlab exception: e.what() std::endl; } // 释放运行时 mclTerminateApplication(); return 0; }几个关键点mclInitializeApplication必须在所有Matlab库函数之前调用否则创建PolyfitLib实例时会崩溃。释放时对应mclTerminateApplication。mwArray构造函数的参数含义是“行数、列数、类型”。mwArray x(6, 1, mxDOUBLE_CLASS)表示6行1列double类型矩阵也就是列向量。SetData把裸指针数据从C拷贝到Matlab数组中。GetData反向操作从Matlab数组拷回来。如果m函数返回的不是列向量而是行向量x和y初始化时行列参数要反过来。Matlab本身的排序规则是列优先和C/C的行优先不同取数据时要注意坐标换算。提示mwArray自带管理内存但它的构造函数和拷贝构造函数内部会引用计数千万不要手动对mxArray*调用mxDestroyArray那会造成重复释放。所有Matlab数据都应该通过mwArray对象生命周期来管理。4.4 Debug和Release分开配置工程属性里的附加包含目录和附加库目录这样配好了但Debug和Release要分别设置。尤其是链接器的选项Debug链接的是PolyfitLib.lib里的Debug版本接口实际是根据宏区分Release链接的是Release版本接口。两个配置不能共用一份设置否则运行时会因为CRT不一致导致内存错误。另外VS工程运行调试前别忘记把PolyfitLib.dll和Matlab Runtime的dll拷贝到exe所在目录一般就是工程的Debug或者Release输出目录。更省事的办法是在工程属性“调试-环境”里设置PATHC:\Program Files\MATLAB\R2020b\runtime\win64;D:\my_project\3rdparty\PolyfitLib\bin\;%PATH%这样每次重新编译后自动能找到不用手动拷贝。我在实际测试的第一版demo里就是漏了这一步编译链接全通过一运行弹窗报“找不到PolyfitLib.dll”当时还以为是头文件路径配错了折腾了一圈才发现是运行时找不到dll。5. 在Qt工程里的集成5.1 工程迁到Qt之后的区别如果你的最终产品是用Qt写的上面VS里的思路基本可以平移但有几个环节在Qt里必须重新配置。最大的区别是Qt的pro文件负责管理所有编译链接规则你不能再用VS的属性面板去设置而是要把所有的include路径、lib路径、附加依赖项写进pro文件。我先说明一下这里说的Qt是指用MSVC编译器比如MSVC2017_64套件来构建工程。如果你用MinGWMatlab生成的msvc格式库是没办法链接的别浪费时间了。还有就是Qt Creator里选择编译器套件的时候要看清楚用的是哪个MSVC版本得和Matlab官方支持的版本匹配。5.2 pro文件的关键配置假设我把PolyfitLib相关文件放在工程同级的3rdparty\PolyfitLib目录下pro文件里这样写TEMPLATE app TARGET QtCallMatlabDemo QT core gui greaterThan(QT_MAJOR_VERSION, 4): QT widgets CONFIG c11 # 定义库的安装路径 POLYFIT_DIR $$PWD/../3rdparty/PolyfitLib MATLAB_ROOT D:/Program Files/MATLAB/R2020b INCLUDEPATH \ $$POLYFIT_DIR/include \ $$MATLAB_ROOT/extern/include \ $$MATLAB_ROOT/extern/include/win64 LIBS \ -L$$POLYFIT_DIR/lib \ -L$$MATLAB_ROOT/extern/lib/win64/microsoft \ -lPolyfitLib \ -lmclmcrrt \ -llibmat \ -llibmx # 运行时dll所在目录用冒号分隔追加到PATHWindows上qmake用;分隔 win32 { DESTDIR $$PWD/bin QMAKE_POST_LINK $$quote(cmd /c copy /Y $$POLYFIT_DIR/bin/PolyfitLib.dll $$DESTDIR\) } SOURCES \ main.cpp几点说明MATLAB_ROOT里如果路径带空格pro文件里又会解析出问题。实测如果Matlab装在C:\Program Files\MATLAB\R2020b在INCLUDEPATH里带空格的路径需要谨慎处理。最简便的办法是把Matlab的include和lib目录做成符号链接或者设置环境变量MATLAB_ROOT然后在pro里引用环境变量。-lPolyfitLib对应的文件名在Windows上是PolyfitLib.lib。qmake的-l选项会自动到_ -L指定的目录里找lib文件不需要手动写全名。QMAKE_POST_LINK的作用是在编译完成后自动把生成的dll复制到exe输出目录。这只是图省事如果嫌麻烦也可以手动拷贝一次。5.3 在Qt界面里调用Matlab函数的代码组织既然用了Qt调用Matlab函数就不该再往main里堆了。建议封装一个类比如MatlabFittingService把Matlab库的初始化、调用、数据转换全部封装起来界面层只负责传参数、拿结果。// matlabfittingservice.h #ifndef MATLABFITTINGSERVICE_H #define MATLABFITTINGSERVICE_H #include QObject #include QVector #include PolyfitLib.h struct FitResult { QVectordouble coeffs; QVectordouble yfit; bool success false; QString errorMessage; }; class MatlabFittingService : public QObject { Q_OBJECT public: explicit MatlabFittingService(QObject *parent nullptr); ~MatlabFittingService() override; bool initialize(); FitResult fit(const QVectordouble x, const QVectordouble y, int order); private: bool m_initialized false; PolyfitLib* m_lib nullptr; }; #endif // MATLABFITTINGSERVICE_H对应的cpp#include matlabfittingservice.h #include QDebug #include mwArray.h #include mclmcrrt.h MatlabFittingService::MatlabFittingService(QObject *parent) : QObject(parent) { } MatlabFittingService::~MatlabFittingService() { delete m_lib; m_lib nullptr; if (m_initialized) { mclTerminateApplication(); m_initialized false; } } bool MatlabFittingService::initialize() { if (m_initialized) return true; if (!mclInitializeApplication(nullptr, 0)) { qWarning() Matlab runtime initialize failed; return false; } try { m_lib new PolyfitLib(); m_initialized true; return true; } catch (const mwException e) { qWarning() PolyfitLib construct failed: e.what(); mclTerminateApplication(); return false; } } FitResult MatlabFittingService::fit(const QVectordouble x, const QVectordouble y, int order) { FitResult result; if (!m_initialized || !m_lib) { result.errorMessage QStringLiteral(Matlab service not initialized); return result; } try { const int len x.size(); mwArray mwX(len, 1, mxDOUBLE_CLASS); mwArray mwY(len, 1, mxDOUBLE_CLASS); mwArray mwOrder(1, 1, mxDOUBLE_CLASS); // QVectordouble - double[] - mwArray QVectordouble xD x; // deep copy QVectordouble yD y; mwX.SetData(xD.data(), len); mwY.SetData(yD.data(), len); int n order; mwOrder.SetData(n, 1); mwArray mwCoeffs; mwArray mwYfit; // 调用实际的Matlab编译函数 m_lib-polyfit_demo(2, mwCoeffs, mwYfit, mwX, mwY, mwOrder); // 取数据 int coeffCount (int)mwCoeffs.NumberOfElements(); QVectordouble coeffsBuf(coeffCount); mwCoeffs.GetData(coeffsBuf.data(), coeffCount); int fitCount (int)mwYfit.NumberOfElements(); QVectordouble yfitBuf(fitCount); mwYfit.GetData(yfitBuf.data(), fitCount); result.coeffs coeffsBuf; result.yfit yfitBuf; result.success true; } catch (const mwException e) { result.errorMessage QString::fromLocal8Bit(e.what()); qWarning() Matlab call exception: e.what(); } return result; }几个细节mwArray::GetData要求传入的缓冲区大小至少是数组元素总数我通常用NumberOfElements()先拿到元素个数再申请QVector这样最稳妥不会越界。SetData需要的缓冲区在函数调用期间有效即可Matlab会自己拷贝所以我可以直接用QVector的data()指针传进去。initialize和析构配对。如果Qt程序里多次构造和析构这个服务类每次都要保证mclInitializeApplication和mclTerminateApplication成对出现否则运行时里的全局状态会错乱表现为第二次打开窗口时崩溃。实际项目里MatlabFitService可以做成单例因为Matlab运行时初始化一次就够了反复初始化释放会影响性能。如果你在界面里频繁调用Matlab函数务必把服务对象在程序存活期间一直保持不要每次调用都创建、销毁。5.4 Qt工程里怎么调试Matlab相关代码在Qt Creator里调试这种混合工程有一个技巧把Matlab提供的for_testing目录里生成的那个测试exe先跑一遍确认库本身没问题再跑Qt工程。如果测试exe正常而Qt工程崩那问题基本出在Qt的配置上比如编译器套件选错、运行时路径没成功复制。Qt Creator自带的调试器对mwArray内部的变量展开不好你直接观察mwArray的内容看着很痛苦。我自己的做法是在关键位置打日志把返回值先转成double数组再打印出来核对用输出数值来判断Matlab函数有没有被真正调起来。另外如果Qt工程用了中文路径或者中文字符串调用Matlab后返回的报错信息经常带乱码。这个问题在Matlab R2020b上尤其明显。我建议在封装类里统一把错误信息转成QString::fromLocal8Bit能解决一大半乱码问题。如果还乱就把Matlab函数里的错误处理提前写好主动抛出简洁英文信息省得解析Matlab的调用栈。6. 常见问题与排查技巧实录6.1 问题速查表我把实际踩过的坑和排查思路整理成一个表每个问题都是真实遇到过的问题现象根本原因解决方案编译提示找不到PolyfitLib.hinclude路径没配对把3rdparty的include目录加到工程配置并且区分Debug/Release链接时报LNK1104无法打开lib文件lib路径错误或lib文件名不对检查Matlab的lib目录是extern/lib/win64/microsoft不是microsoft下再嵌套一层链接报一堆LIBCMT与LIBCMTD冲突运行库设置不一致C/C代码生成里Debug选/MTdRelease选/MT运行时报找不到PolyfitLib.dllDLL没有在exe目录或PATH中用QMAKE_POST_LINK复制dll或者手动拷到输出目录运行时报“Module not found”或0xc000007b64位/32位混用确保Matlab是64位VS和Qt的工程目标平台也是x64调用函数时程序崩溃mclInitializeApplication初始化失败或未调用在创建任何Matlab对象前初始化并检查返回值调用函数返回错误结果但无异常数据行列顺序不对mwArray构造时行列顺序要和Matlab函数期望一致x如果是行向量要转成列向量Qt里用MinGW编译链接失败编译器套件不兼容必须改用MSVC编译器Matlab库不支持MinGW格式Matlab函数本身报错但catch不到异常类型不是mwExceptioncatch(...)兜底并打印Matlab的日志6.2 一组非常隐蔽的坑第一mwArray的类型。如果你Matlab函数里用的是uint8或者logicalC侧mxDOUBLE_CLASS传进去自动转换未必总是符合预期。尤其是图像处理类函数图像矩阵通常是uint8而Matlab里很多操作对类型敏感的。正确做法是把类型常量改称mxUINT8_CLASS并用mxUint8的数据缓冲区和SetData配合。这属于混合编程里数据类型的经典痛点一定要提前规划好m函数输入的类型。第二字符串参数怎么传。如果Matlab函数需要字符串参数不能用mxDOUBLE_CLASS构造mwArray应该用mwArray(str, char)这种重载。比如传一个文件名给Matlab里的load函数就是mwArray filename(data.mat, char)。这个重载很多人不知道文档里也不太好找我第一次用的时候还是翻到本地安装目录里的mclcppclass.h才发现的。第三多输出参数时nargout的个数。如果你的m函数有3个输出C调用时nargout写1也可以Matlab会只计算第一个返回值。但注意如果函数内部有依赖第二个输出值来控制行为的逻辑你这样调用可能会得到不一样的结果。工程上最好固定nargout写清楚注释避免不同调用位置传参个数不一致导致结果莫名不一样。第四QString怎么传给mwArray。上面提到字符串重载但QString和需要的是const char*要先qstr.toUtf8().constData()注意toUtf8返回的临时对象生命周期只到表达式末尾所以不要保存这个指针。如果需要长期保存字符串用QByteArray变量接住再传。6.3 性能上的经验调用Matlab编译后的函数主要有三个耗时环节初始化运行时、数据拷贝、函数内部计算。所谓“Matlab编译后性能好”只针对函数内部计算这一块。数据拷贝的代价在数据量大时其实不小尤其是图像矩阵一帧几百万像素拷进拷出花的时间可能比算法本身还大。实际项目里如果处理的是大数组循环调用我有两个建议尽量把循环放到Matlab函数内部不要在C里循环调用Matlab函数。比如要处理100张图像就构造一个100维的输入参数传进去让Matlab一次跑完而不是C循环100次调用。mwArray复用。不要在每次调用时都重新new一个mwArray可以在类里缓存一些大小固定的mwArray每次SetData更新数据就行可以省掉重复分配内存的时间。另外一个就是Matlab runtime是多线程安全的吗文档里说mclInitializeApplication在整个进程里只要调用一次运行时内部会有锁竞争。如果你的QT程序里多个线程同时调用Matlab函数实测是比较容易出问题。我的做法是把Matlab调用全部丢给同一个工作线程其他线程通过信号槽发指令由这一个线程执行Matlab调用。用一个QThread加一个任务队列能有效避免一堆并发访问mwArray导致的随机崩溃。6.4 给刚上路的人一个建议路线第一次接触这套东西别急着把复杂的算法上线。先建一个最简单的m函数比如两个数相加编译成库在VS里跑通再在Qt里跑通最后再迁移真实算法。这个过程看起来多花了半天时间但排查问题时能大幅缩小范围。我见过不少同事直接上手复杂算法库出了问题不知道是编译环境问题、数据传输问题、还是算法本身问题找个把小时才定位到初始化失败这种低级错误。7. 聊聊我在实际项目中的体会整个方案跑下来我觉得最难的不是某个具体的配置项而是思维模式的转换。写惯了C的人拿到mwArray第一反应是“这玩意儿怎么这么难用”但换个角度想它是为了让Matlab能高效处理多维数组而存在的本身设计得并不差只是你需要按它的规矩来。我的做法是在封装层把所有mwArray细节全部隐藏掉上层只看到简单的FitResult fit(x, y, n)这种接口。这样一来底层是Matlab还是C实现的对上层完全透明。后续如果需要把某个Matlab函数替换成C重写版只需要改封装类的内部实现界面和业务代码一行都不用动。这种“隔离变化”的思路也是这种混合编程架构里最有价值的地方。另外一个感受是官方文档确实重要但别指望一次看懂。我建议把MATLAB_ROOT\extern\include\mclcppclass.h打开里面对mwArray的每个方法都有注释比大多数二手教程都准确。遇到不确定的API直接查这个头文件比去网上搜效率高多了。最后再分享一个小技巧Matlab生成的C库如果算法更新了只要函数签名名称、输入输出个数、每个输入输出的含义不变你只需要重新编译生成DLL替换掉旧DLL就行C工程代码完全不用改。我充分利用这个特性把Matlab侧的算法迭代从“周更”压缩到了“小时更”改完函数直接重新打包Qt程序重新运行就生效。这一点是我觉得这种集成方式最爽的地方值得你在方案设计阶段就有意识地保持接口稳定。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进