☰
MATLAB生成DLL在VS2017中的调用:原理、配置与mwArray实战
2026/10/16 1:22:24 网站建设 项目流程

简介:面向需要在 Visual Studio 中调用 MATLAB 生成 DLL 的 C++ 开发者,这份整理教程系统梳理了从 MATLAB 封装到 VS 调用的完整链路:设置编译环境、选择 C++ 代码生成、通过 Library Compiler 完成打包,随后在 VS 中配置 VC++ 目录、链接器附加库目录与附加依赖项,并执行 mbuild -setup 指定编译语言。资源以 1 个 docx 文档承载,压缩包约 1.08MB,适合作为混编开发的快速查询手册。文档内附完整测试程序源码,演示 mwArray 数组类的赋值与函数调用,例如对 a、b 赋值后通过 test(1,z,a,b) 完成加法运算,同时覆盖 mclInitializeApplication、testInitialize、testTerminate 等关键接口,并列出 mclmcr.lib、libmx.lib、libmat.lib 等必需的链接库文件;针对环境初始化失败等常见问题也提供了排查思路。已有 2553 人学习,对于 MATLAB 与 C++ 交互开发的初学者或需要快速落地 DLL 调用的工程人员,能显著缩短环境配置与代码调试时间。

1. 算法写好了,产品端怎么把 MATLAB 函数用起来

做算法的人大概率都遇到过这个场景:MATLAB 里跑通了一段计算,仿真结果也很漂亮,但产品端是 C++ 写的,总不能为了一个函数把整套 MATLAB 运行时都塞进客户机器里。这几年我拆过不少 MATLAB 生成 DLL 再被 VS 调用的案例,结论是这条路完全走得通,但中间环节确实多:MATLAB 端要封装、VS 端要配路径、链接器要挂 lib、代码里还得处理 mwArray 这种特殊类型,任何一个环节脱节都会翻车。这篇笔记围绕 VS2017 调用 MATLAB 2018b 生成的 DLL 这条主线,把封装步骤、环境配置、调用格式和几个容易踩的坑完整捋一遍,适合正在做跨语言集成的工程师,也适合第一次接触 mwArray 的初学者照着操作。

2. MATLAB 端封装 DLL:先搞清楚 mcc 和 MCR 的关系再动手

2.1 封装原理:为什么 DLL 不能脱离 MCR 单独运行

MATLAB 生成 DLL 的底层工具不是编译器本身,而是 mcc,它负责把 .m 函数翻译成 C++ 共享库。这里有一个非常关键的概念:生成的 DLL 并不包含 MATLAB 的数学内核,它只是对 MATLAB Runtime(MCR)的调用封装。也就是说目标机器上如果没有安装对应版本的 MCR,DLL 就算拷贝过去也跑不起来,会直接报“无法加载 DLL”或者入口点找不到。

这个机制决定了封装前的环境准备是有优先级的。我一般会先执行一次 mbuild -setup,把编译语言指向已经安装的 C++ 编译器。在 MATLAB 命令行窗口输入这个命令,系统会列出可用编译器,选择 Microsoft Visual C++ 2017 对应的选项即可。这一步如果跳过,后面 Library Compiler 界面里可能找不到可用的 C++ 编译链路,封出来的 DLL 在 VS 里链接时会报一堆莫名其妙的 LNK 错误。

除了 mbuild -setup,还要顺手确认一下 mex -setup C++,两者指向同一个编译器工具链。常见做法是直接看 MATLAB 的“预设”里 C/C++ 编译器是否已经识别到 VS2017,识别到了再走封装流程会顺畅得多。

2.2 Library Compiler 封装步骤:选对“C++ Shared Library”而不是别的

在 MATLAB 的 App 选项卡里找到 Library Compiler,打开后左侧需要选择 Library Type。这里最容易选错:如果选了 Windows Dynamic Library,生成的是 C 接口;要做 C++ 调用,必须选择 C++ Shared Library。

主界面里把写好的 .m 文件添加进“Exported Functions”区域,比如我经常拿来做演示的 test.m:

function Y = test(X1, X2) Y = X1 + X2; end

然后点击右侧的 Settings 齿轮图标,在 Language 里确保选的是 C++,这样生成的 .h 头文件里会自动带上 mwArray 相关的类型声明。最后点 Package 按钮,MATLAB 会执行编译并生成一个文件夹,里面包含的头文件 test.h、动态库 test.dll、导入库 test.lib,以及一个可选的 setup 脚本。

这里要留意一个细节:生成的文件夹里通常还包含一个子文件夹,里面放着 for_redistribution 和 for_testing 两份内容。VS 做开发调试时用的是 for_testing 里面的 DLL 和 lib;如果要打包发布给没有 MATLAB 的机器,需要运行 for_redistribution 里的安装包,那个安装包会把 MCR 运行时一并装到目标机器上。开发阶段先用 for_testing 目录下的文件就够了。

封装完成后,可以在 MATLAB 命令行里输入:

!dir

查看当前工作目录,确认 test.h、test.dll、test.lib 三个文件是否已经生成。这三个文件是后续 VS 工程里最核心的三样东西,缺一个都没法往下走。

3. VS2017 环境配置:包含目录、附加库目录和链接器依赖项

3.1 配置目录的四个通道:工程属性里到底要填哪些路径

第一步是打开 VS2017,创建一个空的 C++ 控制台应用程序,然后把 MATLAB 生成的 test.h、test.dll、test.lib 拷贝到项目目录下,我一般习惯在工程文件夹里建一个 external 目录专门放这些外部依赖,这样整个项目复制到别的电脑上时不会因为绝对路径失效而报错。

接着打开项目属性页,先找到“VC++ 目录”这个配置项。这里需要填两个路径:一个是“包含目录”,要指向 MATLAB 安装目录下的 extern\include 文件夹,因为 test.h 内部会 include MATLAB 提供的头文件;另一个是“库目录”,要指向 extern\lib\win64\microsoft 文件夹,这里放着 mclmcrrt.lib 等 MATLAB 运行时库的导入库文件。

这里有一个小知识点:VS 里的“VC++ 目录”和“链接器-常规-附加库目录”功能上有重叠。我常用的分工是,在 VC++ 目录里填 MATLAB 自带头文件和库文件的路径,在链接器里填我自己的库文件路径(比如项目里的 external 目录)。当然有工程师喜欢把路径全部塞进链接器里也完全没问题,关键在于保证所有需要的路径都被覆盖到。

第二步是配置链接器。打开“链接器-常规-附加库目录”,把我拷贝了 .lib 文件的路径填进去;再打开“链接器-输入-附加依赖项”,把需要链接的 lib 文件名称逐行加上。这里不是只加 test.lib 就完事的,MATLAB 生成的 C++ 库还依赖一组 MATLAB 运行时的 lib,我的经验是完整的依赖清单可以参照下表配置。

lib 文件名称作用说明
mclmcr.libMATLAB C++ 共享库运行时入口
mclmcrrt.lib运行时核心库(通常已经包含在 mclmcr 中)
mclbase.lib基础服务层,初始化环境时用到
mclcommain.lib命令行应用辅助库
libmex.libMEX 接口兼容库
libmx.lib矩阵数据类型的底层库
libmat.libMATLAB 数组访问接口
libeng.lib引擎调用支持(非必须但建议加上)
test.lib自己封装的函数的导入库

注意这里有个细节:这些 lib 文件大多数是 64 位版本,如果 VS 工程配置的是 Win32 平台,链接时会报“无法打开文件 mclmcr.lib”,因为 win64 目录下根本不存在 32 位库。所以一旦看到这类错误,先检查平台是不是被切到 x64 了。

3.2 Release / Debug 一致性和位数检测:编译不通过时先查这两项

配置完路径后编译,最常见的报错有两类:一类是 LNK 1104 找不到某个 .lib 文件,这种通常是附加库目录没配对或者文件名拼写有误;另一类是 LNK 2019 无法解析的外部符号,这种问题往往出在 Release 和 Debug 模式下链接的运行时库不一致。MATLAB 生成的 DLL 默认是 Release 编译的,VS 工程如果跑在 Debug 模式下,配置的“运行时库”如果是 /MTd 或者 /MDd,就有可能出现 mwArray 相关符号能声明但链接不过的情况。

我的做法是:整个项目统一使用 Release 模式调试。如果确实需要在 Debug 模式下跑,可以去 MATLAB 安装目录下找是否有对应的 debug 版本的运行时库,但一般情况下不值得折腾,直接用 Release 就好。

另外有个简单的验证方法:打开 VS 的“生成-输出”窗口,看链接命令里实际使用的 lib 路径是不是我们配置的那些目录。如果链接命令里出现的是 C:\Windows\System32 或者别的奇怪路径,说明附加库目录的顺序或者全局搜索路径有干扰,把项目自己的路径往前提即可。

配置完成后再编译一次,如果能够顺利生成 .exe,就说明路径这块已经通了,接下来进入代码层面。

4. C++ 调用代码与 mwArray 用法:从初始化到释放的完整链路

4.1 调用格式的核心约定:三个参数位置的含义

在写代码之前,先理解 MATLAB 生成的函数的调用格式。比如 test.m 的签名是 Y = test(X1, X2),那么在生成的 test.h 里,对应的函数声明大致是:

void test(int nlhs, mwArray& Y, const mwArray& X1, const mwArray& X2);

这里 nlhs 表示输出参数的个数,之后依次是输出参数、输入参数。MATLAB 生成的头文件里每个导出的函数都遵循这个约定:第一个参数是输出数量,紧接着是输出参数列表,再往后才是输入参数列表。这个格式很容易记错,我最开始就把输出参数漏掉了,结果调用后 z 里始终是初始值。

理解了签名之后,再来看完整的调用流程。代码的骨架分为五个阶段:初始化 MATLAB 应用环境、初始化具体函数库、构造输入输出数据、调用函数、释放环境。下面这段代码是完整的测试程序,可以直接复制到 VS 工程中使用。

4.2 可运行的测试代码及逐段拆解

#include "test.h" #include <iostream> using namespace std; int main() { bool isOk = 0; if (!mclInitializeApplication(NULL, 0)) { cout << "Could not initialize the application.\n"; return -1; } cout << "isOk = " << isOk << endl; // 此时尚未初始化函数库 isOk = testInitialize(); cout << "isOk = " << isOk << endl; // 返回 true 表示函数库就绪 mwArray a(1, 1, mxDOUBLE_CLASS); mwArray b(1, 1, mxDOUBLE_CLASS); a(1, 1) = 1.8; b(1, 1) = 2.9; mwArray z(1, 1, mxDOUBLE_CLASS); test(1, z, a, b); cout << a << "+" << b << "=" << z << endl; testTerminate(); mclTerminateApplication(); std::cout << "C++完成matlab的dll调用" << std::endl; int aa; std::cin >> aa; return 0; }

逻辑说明:第一段 mclInitializeApplication(NULL, 0) 是启动 MATLAB 运行时环境,NULL 表示没有任何启动选项,0 表示选项个数为 0。这一步如果不做,后面所有 mwArray 的构造和调用都会在运行时崩溃,而且不会给出具体提示,这是最容易踩的坑之一。

第二段 testInitialize() 是对当前 DLL 的函数库做初始化。这里返回的 isOk 才是真正意义上的“库是否就绪”标志。在调用任何导出函数之前,两个初始化都必须成功。代码里加了一段 cout 打印 isOk 的中间值,是刻意展示初始化前后的状态变化,方便新手确认执行到的位置。

第三段 mwArray 的构造是重点。mwArray a(1, 1, mxDOUBLE_CLASS) 表示创建一个 1 行 1 列的 double 类型数组,后面的括号索引 a(1, 1) 是 MATLAB 风格的下标,注意不是 C 风格的 [0][0],而是从 1 开始。赋值 1.8 和 2.9 之后,调用 test(1, z, a, b),第一个参数 1 表示函数返回一个输出,z 是输出参数,a 和 b 是传给 MATLAB 函数的输入。

输出参数 z 在调用前也需要用 mwArray 构造出来,虽然它的值会被函数覆盖,但不构造直接传引用会触发访问冲突。这段代码里 z 的构造维度是 (1, 1, mxDOUBLE_CLASS),与预期返回的标量一致。如果 MATLAB 函数返回的是一个向量或矩阵,这里就要按实际尺寸构造 mwArray。

最后一段 testTerminate() 和 mclTerminateApplication() 是对应的释放函数,前者释放 DLL 函数库的资源,后者关闭 MATLAB 运行时,顺序不能颠倒。我见过有同学只写 mclTerminateApplication 不写 testTerminate,程序虽然能退出,但会有内存泄漏警告。

4.3 mwArray 赋值和读取的进阶细节

mwArray 的操作符重载做得比较完整,除了第一维索引赋值,还可以用 GetData 方法读取数据:

double result = 0.0; z.GetData(1, 1, result); cout << "result = " << result << endl;

GetData 的第一个参数是输出类型指针或者标量引用,后面的参数是索引位置。如果 z 是一个矩阵,需要先调用 z.NumberOfElements() 获取元素个数,再循环读出。这个接口在没有 MATLAB 环境的情况下也可以使用,因为它依赖的仅仅是运行时库。

对于多维数组的场景,比如 MATLAB 函数返回一个 2 行 3 列的矩阵,构造时可以直接写 mwArray m(2, 3, mxDOUBLE_CLASS),读取时按行列索引 m.GetData(1, 1)、m.GetData(1, 2) 依次取。这里要注意 mwArray 的存储顺序和 MATLAB 相同,是列优先,和 C/C++ 的习惯相反,写循环时容易搞混,建议先打印出来看一遍再写逻辑。

5. 避坑:从 DLL 加载失败到数据错乱的六个翻车现场

5.1 现象:“无法加载 DLL”或者“找不到指定模块”

原因:MCR 没有安装,或者装了但是版本和编译时不一致。这里说的版本不一致不只是 2018b 和 2019b 这种大版本差异,即使是同一大版本下的小更新,运行时也可能不兼容。

解决:开发机上确认 MATLAB 自带运行时目录已经存在,比如 C:\Program Files\MATLAB\MATLAB Runtime\v95 这种路径是否可访问。如果目标机器要部署,就用 for_redistribution 目录里的 exe 安装一遍运行时,不要试图手动拷贝 mclmcrrt.dll 来省事,这个 DLL 本身还依赖其他组件,拷贝不全照样起不来。

5.2 现象:编译报了 LNK 2019,说不认识 mwArray 相关符号

原因:最常见的情况是包含目录指到了 extern\include\win64 而不是 extern\include,导致头文件里条件编译没有打开 64 位分支,类型定义和链接库对不上。

解决:在 VC++ 包含目录里同时加上 extern\include 和 extern\include\win64 两个路径,顺序不要紧,但两个都不能少。另外检查预处理定义里是否有 _WINDOWS 相关的宏,MATLAB 的头文件在某些宏缺失时会跳过一部分声明,加上 WIN32 和 _WINDOWS 后重新编译,问题基本能消除。

5.3 现象:程序编译和链接都过了,一运行就崩,崩溃位置指向 mclInitializeApplication

原因:初始化调用放在了静态变量构造之后,或者 mwArray 类型的全局对象先被构造了。MATLAB 的运行时环境要求在第一次创建任何 mwArray 对象之前完成初始化。

解决:不要在全局作用域声明 mwArray 类型的变量,把所有 mwArray 的使用都在 mclInitializeApplication 之后进行。如果工程里非要保留全局对象,改成指针形式然后在 main 里手动 new,也算是一种绕法。

5.4 现象:调用 test() 之后 z 的值始终是 0,不报错也不崩溃

原因:nlhs 参数写错。比如 MATLAB 函数返回两个值,但在 C++ 里只给了 1 个输出参数位置,生成的函数内部可能只会取第一个输出槽位,第二个参数被当成输入使用了。更隐蔽的情况是 nlhs 传了 0,函数被当成“无输出调用”,z 自然不会被写入。

解决:先在 MATLAB 命令行里用 nargout 查看函数实际输出个数,再检查 C++ 调用语句里 nlhs 是否与该值一致。我自己反复出错后养成了一个习惯,每写一个调用就先数一下参数个数,从左往右依次是 nlhs、输出、输入,从来不看函数文档,直接对照生成的头文件。

5.5 现象:编译到一半报“检测到 _ITERATOR_DEBUG_LEVEL 不匹配”

原因:这是 Debug 和 Release 的运行时库混用导致的经典报错。MATLAB 的运行时库是 Release 编译的,VS 工程开了 Debug 模式,并且预处理器里 _DEBUG 被定义,STL 容器和迭代器的调试级别不一致。

解决:直接把整个工程切换到 Release 模式,或者在 Debug 模式下关闭 _DEBUG 宏(不推荐)。最省心的是确认活动解决方案配置是 Release,然后重新编译。

5.6 现象:x64 平台编译成功了,换到 x86 就各种打不开

原因:MATLAB 从 2017 版本开始逐步放弃 32 位支持,生成的 DLL 只有 64 位版本。VS 工程如果默认创建的是 Win32 平台,链接时会在 win64 路径下找不到 lib 文件。

解决:进入“配置管理器”,把活动解决方案平台切换为 x64。如果列表里没有 x64,先新建一个平台,再重新配置包含目录和附加库目录,因为不同平台下路径配置是独立的,切换后需要重新检查一遍。

6. 进阶:从控制台测试到工程集成的三个关键习惯

当控制台测试程序能顺利算出 1.8 + 2.9 = 4.7 之后,真正的工程场景才算开始。实际项目里很少直接在一个 main 函数里完成所有事情,更多是把 MATLAB 的 DLL 封装进一个 C++ 类,在业务代码里创建对象、调用方法、释放资源。这个过程中有几个习惯非常值得固定下来。

第一个习惯是用 mclRunMain 代替裸写 main 入口。mclRunMain 是 MATLAB 提供的一个辅助函数,它会自动处理 mclInitializeApplication 的前置条件,并且保证在 main 真正执行前初始化工作已经完成。写法大致是:

int runMain(int argc, char** argv) { // 业务逻辑里可以直接构造 mwArray return 0; } int main(int argc, char** argv) { return mclRunMain((mclMainFcnType)runMain, argc, argv); }

把业务逻辑放进 runMain 之后,不再需要在代码里手动调 mclInitializeApplication 和 mclTerminateApplication,这两个由 mclRunMain 负责兜底。如果未来要封成 Windows 服务或者嵌入到其他框架里,这个写法比裸 main 更不容易出问题。

第二个习惯是定期用 MATLAB 自带的 Dependency Analyzer 检查生成的 DLL 依赖了哪些运行时组件。打包部署时被用户反馈“换个机器就起不来”,绝大多数是运行时缺组件。我会在发布前把 for_testing 目录里的完整文件列表导出来,对照部署机器的环境逐项勾选,确认 mclmcrrt 版本一致再发版本。

第三个习惯是验证 DLL 更新是否生效。MATLAB 改了一版 test.m 重新编译 DLL 后,VS 工程里如果之前已经把 DLL 拷贝到外部目录,容易发生“代码更新了但跑的还是旧 DLL”。我通常会把 DLL 的生成时间打印到日志里,或者在初始化函数里加一句版本号输出。第一次发现这个问题是在一个反复调试边界条件的场景里,改了 MATLAB 代码重新编译后 C++ 程序结果纹丝不动,查了半天发现外部目录的 DLL 还是上一个版本,从那以后我每次替换 DLL 后都强制走一遍“拷贝-验证时间戳-重跑单测”三个动作,再也没被缓存问题坑过。

这套链路完整走通一次之后,后续再遇到 MATLAB 和 C++ 集成会顺手很多,希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询