本文记录针对一次 MATLAB MEX 文件跨平台(Windows/Linux)运行的异常调试过程。

环境介绍

算法说明

算法使用 MATLAB 2018a for Windows 打包,编译后以 MatlabComputer.jar 输出。
简单的说,在算法内部会有三个步骤,首先调用 a()b(),然后将结果传入 c() 计算得到最终值。
由于算法是打包成 jar 包,因此也以 Java 平台调用 MATLAB 算法为例:

  1. // 导入 MATLAB 相关包
  2. import com.mathworks.toolbox.javabuilder.MWException;
  3. import com.mathworks.toolbox.javabuilder.MWStructArray;
  4. // 导入算法包
  5. import MatlabComputer.MatlabComputer;
  6. public class MatlabTest {
  7. public static void main(String[] args) {
  8. String[] data = readTestData("test.json");
  9. // 实例化算法
  10. MatlabComputer mc = new MatlabComputer();
  11. // 由MATLAB编译而来的方法,首个参数总是返回值个数,后续跟着的才是正常的入参
  12. // 同样的,返回结果将会变成数组,因此要根据返回值个数获取实际值
  13. // 示例中,a、b 方法都需要 1 个入参并返回 1 个结果
  14. Object[] resultA = mc.a(1, data[0]);
  15. Object[] resultB = mc.b(1, data[1]);
  16. // c 方法则需要 2 个入参,返回的结果则同样是 1 个
  17. Object[] resultC = mc.c(1, resultA[0], resultB[0]);
  18. System.out.println("-----completed-----");
  19. // 示例方法的最终计算结果是一个结构体,因此进行类型转换,方便后续解析
  20. MWStructArray struct = (MWStructArray) resultC[0];
  21. // 注意,MATLAB 对象的索引是从 1 开始的
  22. for(int i = 1; i < struct.getDimensions()[1]; i++) {
  23. // MWStructArray 提供 getField 方法获取
  24. System.out.println(" name: " + struct.getField("name", i));
  25. System.out.println("value: " + struct.getField("value", i));
  26. System.out.println();
  27. }
  28. }
  29. }

可行的 Windows 环境

关于算法的调用测试,已经在以下环境中通过:

测试使用的 Linux 环境

这里使用 Docker 来构建含有 MCR 与 JDK 环境的镜像,当然也可以选择使用虚拟机、实体系统等方式。

  1. # 不建议使用该 dockerfile 构建镜像,仅供示例,没有做任何容量的优化。
  2. # 推荐参考微软 dotnet core 的 FROM xx as yyy 构建方式来压缩镜像体积。
  3. FROM ubuntu:latest
  4. ARG DEBIAN_FRONTEND=noninteractive
  5. # sources.list 中为阿里的镜像源,用于在国内加速 apt-get 操作
  6. ADD sources.list /etc/apt/
  7. RUN apt-get -q update && \
  8. apt-get install -q -y --no-install-recommends \
  9. xorg \
  10. unzip \
  11. curl && \
  12. apt-get install -q -y build-essential
  13. ADD jdk-8u144-linux-x64.tar.gz /usr/local/java
  14. ENV JAVA_HOME /usr/local/java/jdk1.8.0_144
  15. ENV JRE_HOME ${JAVA_HOME}/jre
  16. ENV CLASSPATH .:${JAVA_HOME}/lib:${JRE_HOME}/lib
  17. ENV PATH ${JAVA_HOME}/bin:$PATH
  18. ADD MCR_R2018a_glnxa64_installer /MCR_R2018a_glnxa64_installer
  19. RUN cd /MCR_R2018a_glnxa64_installer && \
  20. ./install -destinationFolder /opt/mcr -agreeToLicense yes -mode silent
  21. ENV LD_LIBRARY_PATH /opt/mcr/v94/runtime/glnxa64:/opt/mcr/v94/bin/glnxa64:/opt/mcr/v94/sys/os/glnxa64:/opt/mcr/v94/extern/bin/glnxa64
  22. ENV XAPPLRESDIR /etc/X11/app-defaults

接着再生成镜像,此镜像会作为后续试验的基础环境镜像。

  1. docker build -t base-env:mcr2018a-jdk8 .

再以此作为基础镜像,构建含有一个 Java 应用,用于运行算法运行

  1. FROM base-env:mcr2018a-jdk8
  2. EXPOSE 8080
  3. ADD target/matlab-test-1.0.0.jar /app.jar
  4. ENTRYPOINT ["java","-jar","/app.jar"]

那么我们就得到了如下测试环境:

  • ubuntu 20.04
  • OpenJDK 1.8
  • MCR 2018a for Linux

测试与问题排查

第一次尝试-中文路径

首先,尝试直接使用在 MATLAB 2018a for Windows 上编译出来的算法包,得到如下错误:

com.mathworks.toolbox.javabuilder.MWException: Undefined function ‘graphalgs’ for input arguments of type ‘char’.

可以发现,MATLAB 算法的前两个步骤是调用成功(因为没有报错),说明环境是没有问题的。但是异常信息显然是提示我们没找到 graphalgs 这个文件。
这时我们进入容器,查找以下是否存在 graphalgs 这个文件:

  1. # 由于我们不知道这个 graphalgs 文件的全名,因此加上 * 做模糊匹配
  2. find / -name *graphalgs*

结果如下图所示,可以发现中文目录在 Linux 容器中显示为乱码(????):linux.png为了确认这个乱码是什么,我们可以解压 MatlabComputer.jar 文件,得到 MatlabComputer.ctf,即上图中文本文件。

这里也可以通过在测试通过的 Windows 环境下找到 xx 目录来判断。 in-windows.png

通过 URL decode 后发现是中文「数据输入」,推测可能是中文目录影响了 MCR 的路径索引,随即联系算法方修改成英文目录后重新编译。

第二次尝试-平台

这次我们得到了纯正英文路径版本的算法包,再次尝试,得到了同样的结果:

com.mathworks.toolbox.javabuilder.MWException: Undefined function ‘graphalgs’ for input arguments of type ‘char’.

还是没找到 graphalgs 文件!这时我们注意到 graphalgs.mexw64 文件的后缀名,总所周知后缀名是描述文件的一个附加属性,那么 w64 是不是指得就是 w(indows)64——此文件适用于 Windows 64 位平台呢?!
从官网对 MATLAB 的 MEX 文件描述中可以发现,对于不同的运行平台是需要使用不同的 MEX 文件的,这正好证明了我们的猜想——我们需要一个 graphalgs.mexa64 文件。

虽然 MATLAB 脚本和函数的扩展名 .m.mlx 独立于平台,但 MEX 函数具有如下所示的 64 位平台特定的文件扩展名:

  • Linux® - .mexa64
  • Apple macOS - .mexmaci64
  • Microsoft® Windows® - .mexw64

第三次尝试-Github

我们通过 bing 搜索了相关的资料,发现在 Github 某些仓库中有 graphalgs.mexa64 文件,于是获取后再次编译算法并测试。然后我们得到了一个新的错误:

Invalid MEX-file ‘/root/.mcrCache9.4/Matlab0/toolbox/bioinfo/graphtheory/private/graphalgs.mexa64’: /root/.mcrCache9.4/Matlab0/toolbox/bioinfo/graphtheory/private/graphalgs.mexa64: undefined symbol: _ZN6matrix6detail10noninlined12mx_array_api11mxGetStringEPK11mxArray_tagPcm.

graphalgs.mexa64 文件生效了,看来问题确实是平台不同引起的,但它显然和原版的 graphalgs.mexw64 在内容上有点不一样。
再次和算法方沟通后发现,原来算法包中的 graphalgs.mexw64 文件是从 Windows 上 MATLAB 安装目录下 .\toolbox\bioinfo\graphtheory\private 直接获取的。

那么,重点来到了版本

如果你使用过多个版本的 MATLAB 就会发现,高版本对于低版本编译的产物兼容性并不是很好(甚至好像都没有能兼容的),于是继续从官方翻到了关于 MEX 版本兼容性文档:

为了获得最佳效果,您的 MATLAB® 版本必须与用于创建该 MEX 文件的版本相同。

MEX 文件使用 MATLAB 运行时库。在早期版本的 MATLAB 上创建的 MEX 文件通常可在 MATLAB 的更高版本上运行。如果 MEX 文件出现错误,请从源代码重新编译 MEX 文件。

在 MATLAB 的较新版本上创建的 MEX 文件有时可以在 MATLAB 的旧版本上运行 ,但我们不提供这一支持。

emmm…「通常可在」的意思就是基本上不行,所以我们需要在 Linux 环境上手动安装一套 MATLAB 2018a for Linux,得到最符合环境的 graphalgs.mexa64 文件。

第四次尝试-还是原配好

安装完成后,我们在相同的目录 .\toolbox\bioinfo\graphtheory\private 下获取到 graphalgs.mexa64 文件,并再次使用 MATLAB 2018a for Windows 打包后,终于是可以正常运行了。

注意,所有的算法编译打包都是通过 MATLAB 2018a for Windows 完成的,即使安装了 MATLAB 2018a for Linux 也仅是提取了其中的一个 graphalgs.mexa64 文件。

总结

总之,官方文档说的对啊!使用 MATLAB 时需注意跨平台带来的问题。

虽然 MATLAB 脚本和函数的扩展名 .m.mlx 独立于平台,但 MEX 函数具有如下所示的 64 位平台特定的文件扩展名:

  • Linux® - .mexa64
  • Apple macOS - .mexmaci64
  • Microsoft® Windows® - .mexw64