創建Matlab engine的python binding

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,有時間再好好研究一下。

發佈了18 篇原創文章 · 獲贊 43 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章