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函数直接编译成DLL(Windows环境下的动态链接库),让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 Runtime(MCR或MWR)即可。运行时体积比完整Matlab小很多,启动速度快。代码以C++类接口暴露,常规C++工程集成很容易。 缺点:编译过程有一定门槛,mwArray等Matlab数据类型需要适应,函数签名设计如果没规划好后面会比较难受。
我最后选的就是第三条路线。因为我的使用场景是做工具软件,既要给别的同事直接用,又不想每个同事的电脑上都装完整Matlab。
提示:如果你预算充足或者只是个人使用,装完整Matlab然后走Engine路线确实调试更直观。但一旦涉及交付、部署、给第三方用,老老实实学Compiler SDK更划算,也更有工程价值。
2. 环境准备与前置配置
2.1 需要的软件版本
我开发时候的环境是:Windows 10(64位)、Matlab R2020b(64位)、Visual Studio 2019、Qt 5.14.2(MSVC2017_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) | 备注 |
|---|---|---|
| R2018a | VS2015、VS2017 | 较新版本指令集兼容性问题较少 |
| R2019b | VS2017、VS2019 | 比较稳定的组合 |
| R2020b | VS2019 | 本文实测环境 |
| R2022a | VS2019、VS2022 | 新版建议直接用VS2022 |
- Qt这边,必须使用MSVC编译套件,不能使用MinGW。因为Matlab生成的库是Microsoft格式的DLL和lib,MinGW链接不上,会报一堆未定义符号。这个属于刚入坑时最容易踩的问题,我有个同事就是卡在这里好几天没想明白。
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.m:
function [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.h:C++头文件,声明了我们的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\include; - Matlab根目录下的
extern\include\win64(里面还有一层子目录需要引用)。
为什么要有Matlab的include目录?因为PolyfitLib.h里面会include<mclcppclass.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输出目录)。更省事的办法是在工程属性“调试-环境”里设置PATH=C:\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套件)来构建工程。如果你用MinGW,Matlab生成的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 += c++11 # 定义库的安装路径 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所在目录,用冒号分隔追加到PATH(Windows上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 { QVector<double> coeffs; QVector<double> 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 QVector<double>& x, const QVector<double>& 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 QVector<double>& x, const QVector<double>& 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); // QVector<double> -> double[] -> mwArray QVector<double> xD = x; // deep copy QVector<double> 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(); QVector<double> coeffsBuf(coeffCount); mwCoeffs.GetData(coeffsBuf.data(), coeffCount); int fitCount = (int)mwYfit.NumberOfElements(); QVector<double> 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.h | include路径没配对 | 把3rdparty的include目录加到工程配置,并且区分Debug/Release |
| 链接时报LNK1104无法打开lib文件 | lib路径错误或lib文件名不对 | 检查Matlab的lib目录是extern/lib/win64/microsoft,不是microsoft下再嵌套一层 |
| 链接报一堆LIBCMT与LIBCMTD冲突 | 运行库设置不一致 | C/C++代码生成里,Debug选/MTd,Release选/MT |
| 运行时报找不到PolyfitLib.dll | DLL没有在exe目录或PATH中 | 用QMAKE_POST_LINK复制dll,或者手动拷到输出目录 |
| 运行时报“Module not found”或0xc000007b | 64位/32位混用 | 确保Matlab是64位,VS和Qt的工程目标平台也是x64 |
| 调用函数时程序崩溃 | mclInitializeApplication初始化失败或未调用 | 在创建任何Matlab对象前初始化,并检查返回值 |
| 调用函数返回错误结果但无异常 | 数据行列顺序不对 | mwArray构造时行列顺序要和Matlab函数期望一致;x如果是行向量要转成列向量 |
| Qt里用MinGW编译链接失败 | 编译器套件不兼容 | 必须改用MSVC编译器,Matlab库不支持MinGW格式 |
| Matlab函数本身报错但catch不到 | 异常类型不是mwException | catch(...)兜底,并打印Matlab的日志 |
6.2 一组非常隐蔽的坑
第一,mwArray的类型。如果你Matlab函数里用的是uint8或者logical,C++侧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程序重新运行就生效。这一点是我觉得这种集成方式最爽的地方,值得你在方案设计阶段就有意识地保持接口稳定。