创建Matlab engine的python binding

8 篇文章 0 订阅
6 篇文章 2 订阅

Matlab Engine是Mathworks提供的一种混合编程方案,其采用C/S(客户机/服务器)模式,Matlab作为后台服务器,而用户程序(一般是C/C++)通过Matlab Engine提供的函数接口控制服务器执行相应的语句(关于Matlab Engine编程例子参考Matlab自带的engdemo.c,在<MATLABROOT>/extern/exampleseng_mat/)。

本文讨论如何创建Matlab Engine的python binding,以便能够方便地在python中调用Matlab功能。主要提出两种方式:

  1. 使用python标准库ctypes对libeng.dll进行封装。
  2. 利用swig+C/C++创建python扩展文件。

1. libeng.dll主要函数

与Matlab引擎相关的库函数包含在libeng.dll文件中(<MATLABROOT>/bin/win32),只有以下9个导出函数,其函数原型可以参考<MATLABROOT>/extern/include/engine.h文件。

  1. engOpen
    Start matlab process
    启动Matlab服务进程。

  2. engOpenSingleUse
    Start matlab process for single use. Not currently supported on UNIX.
    以独占方式启动Matlab进程,其他用户无法访问当前Matlab进程。

  3. engClose
    Close down matlab server
    关闭Matlab服务进程。

  4. engEvalString
    Execute matlab statement
    在Matlab引擎中执行Matlab语句。

  5. engGetVisible
    GetVisible, do nothing since this function is only for NT
    获取当前窗口状态,指示当前Matlab服务器窗口是否可见。True表示可见,False表示该窗口隐藏。该函数仅对Windows NT系统有效。

  6. engSetVisible
    SetVisible, do nothing since this function is only for NT
    设置当前Matlab服务进程窗口状态。该函数仅对Windows NT系统有效。

  7. engGetVariable
    Get a variable with the specified name from MATLAB’s workspace
    从Matlab服务进程空间中获取指定名称的变量。

  8. engPutVariable
    Put a variable into MATLAB’s workspace with the specified name
    将一个变量以指定名称放入Matlab服务进程空间。

  9. engOutputBuffer
    Register a buffer to hold matlab text output
    注册一个缓冲区以获取Matlab命令执行输出结果。

2. 使用python ctypes封装libeng.dll

由于这几个接口函数都相对比较简单,因而在利用ctypes封装这些函数时甚至都不需要对其输入参数(argtype)和返回参数(restype)格式进行声明,直接利用python的隐式数据类型转换即可完成任务。主要代码如下:

import ctypes

MATLABROOT = "D:\\Program Files\\MATLAB\\R2010a"
libeng = ctypes.CDLL(MATLABROOT + "/bin/win32/libeng.dll")

def engOpen(startcmd=""):
    ep = libeng.engOpen(startcmd)
    return ep

def engOpenSingleUse(startcmd=""):
    ep = libeng.engOpenSingleUse(startcmd)
    return ep

def engClose(ep):
    return libeng.engClose(ep)

def engGetVisible(ep):
    r = ctypes.c_bool(False)
    libeng.engGetVisible(ep, ctypes.byref(r))
    return r.value

def engSetVisible(ep, flag=False):
    return libeng.engSetVisible(ep, ctypes.c_bool(flag))

def engGetVariable(ep, name):
    ptr = libeng.engGetVariable(ep, name)
    return ptr

def engPutVariable(ep, name, value):
    return libeng.engPutVariable(ep, name, value)

def engEvalString(ep, stmt):
    return libeng.engEvalString(ep, stmt)

def engOutputBuffer(ep, buffer=None):
    if buffer is None:
        buffer = ctypes.create_string_buffer('/0'*256)
    return libeng.engOutputBuffer(ep, buffer, len(buffer.value))

下面对代码做两点说明:

  1. MATLABROOT代表Matlab安装目录,可以在Matlab中执行matlabroot命令确定。
  2. libeng.dll中C函数的调用约定为cdecl,故使用ctypes.CDLL('libeng.dll')方式加载。而Windows API中C函数的调用约定为stdcall,所以要使用ctypes.WinDLL('kernel32.dll')方式加载。

3. 使用swig+c创建python扩展文件

利用swig可以直接将C/C++函数build为python扩展模块,从而可以在Python中调用C/C++函数。因而我们可以利用C/C++写一个wrapper,封装对libeng.dll中接口函数的调用,再将wrapper编译连接成一个python扩展模块,就可以在python中导入python扩展模块进行操作。

首先需要安装swig和python distutils模块,之后就可以使用distutils+swig从C/C++代码构建扩展名为.pyd的python扩展文件。其操作流程比较简单,主要是创建一个setup.py脚本和一个扩展名为.i的模块接口文件,该文件用于向swig提供关于C/C++函数原型相关的信息,以便于swig将C/C++函数转化为python扩展。
在命令行运行setup.py进行build和install(python setup.py build install)即可自动完成扩展模块的创建和安装(可以参考Python自带的例子或者网上教程)。

本项目的setup.py脚本如下:

# setup.py

import distutils
from distutils.core import setup, Extension

mod_pymateng = Extension("_pymateng", \
    sources=["src/pymateng.c", "src/pymateng.i"], \
    include_dirs = ["D:\\Program Files\\MATLAB\\R2010a\\extern\\include"], \
    library_dirs = ["D:\\Program Files\\MATLAB\\R2010a\\extern\\lib\\win32\\microsoft"], \
    libraries = ["libeng"]
    )

setup (
    name = "pymateng",
    version = "1.0",
    author = "bigben",
    description = "A python binding to Matlab Engine",
    ext_modules = [mod_pymateng]
)

本项目C语言代码如下:

// pymateng.c
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include "engine.h"

#include <stdint.h>
typedef uint32_t Handle;

typedef struct {
    char *data;
    int size;
    bool valid;
} Buffer;

static Buffer bufferobj = {NULL, 0, false};

int PyReAllocateBuffer(int bufsize) {
    assert(bufsize > 0);

    if (bufferobj.valid == true) {
        fprintf(stdin, "Buffer object is currently in use, please release it first\n");
        return 1;
    }

    char *pbuf = (char *) calloc(bufsize, sizeof(char));
    if (NULL == pbuf) {
        fprintf(stderr, "Faild to allocate space for buffer\n");
        return -1;
    }

    bufferobj.data = pbuf;
    bufferobj.size = bufsize;
    bufferobj.valid = true;

    return 0;
}

int PyReleaseBuffer(void) {
    if (bufferobj.valid == false) {
        return 0;
    }

    free(bufferobj.data);
    bufferobj.data = NULL;
    bufferobj.size = 0;
    bufferobj.valid = false;

    return 0;
}

char *PyGetBufferData(void) {
    if (bufferobj.valid) {
        return (char *)bufferobj.data;
    } else {
        return (char *)(0);
    }
}

/*
 * Start matlab process
 */
Handle PyEngOpen(const char *startcmdstr) {
    Engine * ep = engOpen(startcmdstr);
    return (Handle) ep;
}

/*
 * Start matlab process for single use.
 * Not currently supported on UNIX.
 */
Handle PyEngOpenSingleUse(const char *startcmdstr) {
    int retstatus;
    return (Handle) engOpenSingleUse(startcmdstr, (void*) 0, &retstatus);
}

/*
 * Close down matlab server
 */
int PyEngClose(Handle ep) {
    return engClose((Engine *) ep);
}

/* 
 * GetVisible, do nothing since this function is only for NT 
 */ 
bool PyEngGetVisible(Handle ep) {
    bool flag = false;
    engGetVisible((Engine *) ep, &flag);

    return flag;
}

/*
 * SetVisible, do nothing since this function is only for NT 
 */ 
int PyEngSetVisible(Handle ep, bool flag) {
    return engSetVisible((Engine *) ep, flag);
}

/*
 * Execute matlab statement
 */
int PyEngEvalString(Handle ep, const char *stmt) {
    return engEvalString((Engine *) ep, stmt);
}

/*
 * Get a variable with the specified name from MATLAB's workspace
 */
Handle PyEngGetVariable(Handle ep, const char *name) {
    return (Handle) engGetVariable((Engine *) ep, name);
}

/*
 * Put a variable into MATLAB's workspace with the specified name
 */
int PyEngPutVariable(Handle ep, const char *name, Handle ap) {
    return engPutVariable((Engine *) ep, name, (const mxArray *) ap);
}

/*
 * register a buffer to hold matlab text output
 */
int PyEngOutputBuffer(Handle ep) {
    assert((false != bufferobj.valid) && (NULL != bufferobj.data) && (0 != bufferobj.size));
    return engOutputBuffer((Engine *) ep, bufferobj.data, bufferobj.size);
}

上面的代码存在一个小小的bug:PyEngOpen返回的引擎handle(ep)不能用print命令打印,否则会导致程序崩溃。例如:

ep = PyEngOpen("") % 不加分号

或者

print ep;

会导致程序崩溃。
但是:

ep = PyEngOpen("");

则可以正常工作。之后将ep作为参数传递给其他函数也能正常预定的完成功能。这个bug以后会尝试解决。

在命令行运行以下命令完成扩展模块的创建和安装:

python setup.py build install

运行以下命令进行测试:

python -c "import _pymateng; print(dir(_pymateng));"
python -c "import pymateng; print(dir(pymateng));"

利用C/C++创建python扩展还有很多其他方式,但是基于swig+distutils的方式无疑是其中较简单的一种。因为用户无需对C/C++源文件做任何修改以导入导出Python对象,这些工作全部由swig自动帮你完成。

应当指出,这种利用python扩展创建Matlab engine接口的方式和Numpy或者Scipy中利用本地blas或者lapack库加速线性代数计算的原理是一致的,二者都是通过本地编译python扩展模块的方式调用本地DLL文件。
但是这种方式也有一些弊端,因为这种方式依赖于本地环境,需要利用源码在本地重新构建。而不同机器的环境总是千差万别的,所以很有可能会出现很多问题。

此外还需要注意一个问题:利用python扩展方式封装对本地DLL文件的调用需要确保被调用的DLL文件(例如本项目中的libeng.dll)被加入到系统PATH环境变量中,使其能够被搜索,否则在导入模块时会出现ImportError:DLL load failed 找不到指定模块的错误而导致生成的python扩展模块无法正常导入。

总体来说,这种方式涉及到较多的环境配置,出现问题的可能性相对较大,若配置不当则很难达到预期的功能(这也可以解释为什么很多用户在自己机器上配置Numpy或者Scipy支持本地blas或lapack加速时经常容易出现问题)。其实最主要的原因就是依赖的动态链接库无法正确加载。

一般常见的会有以下两类错误:

  1. ImportError: DLL load failed 找不到指定模块。一般将依赖的DLL文件路径添加到PATH环境变量即可解决这一问题。
  2. ImportError: DLL load failed: %1不是有效的Win32应用程序。这一般是由于在32位系统中调用64位的动态链接库造成的。连接时需要保证版本一致。

本项目中,需要确保MATLABROOT/bin/win32被加入PATH路径,否则pymateng模块导入会失败。命令如下(不同主机上不同Matlab版本直接可能略有差异):

set MATLABROOT=D:/Program Files/MATLAB/R2010a
path %MATLABROOT%/bin;%MATLABROOT%/bin/win32;%path%

以上命令只对当前命令行有效,若要永久生效需要在系统属性中编辑PATH环境变量,将以上路径加入其中。


项目完整代码(包括完整python代码,C语言代码,模块接口文件,setup.py文件以及python测试代码)可从CSDN下载。下载地址:pymateng

注:
截至发此文时才发现网上已经有很多在python调用Matlab engine的实现方案(百度”matlab engine for python”),例如,Mathworks官方已经在Matlab R2016a中提供了MATLAB Engine API for Python,有时间再好好研究一下。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值