在Windows下整合第三方DLL至Python

背景

之前在单位做一项系统的软件改造,由于厂家极为不情愿的只提供了硬件DLL库,而且从接口看,是C++接口,所以我极不情愿的开始用C++写,中间遇到若干坑。在自己撸了一个简易的ORM之后,我实在是受不了C++的开发效率,准备用Python做。

环境

开发环境

  • Windows 7 Ultimate SP1
  • Visual Studio 2015
  • WinPython 32bit 2.7.5
  • 无网络

部署环境

目前未知,目测是

  • Windows 7

项目结构

  • A.dll
  • A.lib
  • A.h(厂家库)
  • reader.h
  • reader.cpp(自己做的封装)

坑一·SWIG

C++转Python库?第一反应就是SWIG,下了回来,按照流程写好了

  • A.i
%module A
%{
#include "A.h"
%}
%include <windows.h>
%include "A.h"

编译:
swig -c++ -python A.i

将生成的A_wrap.cxx和A的库合体编译,出现各种问题:

  • 厂家库采用了大量的结构体+byte数组,被大量的报错
  • 结构体在python中初始化极其复杂甚至不能赋值

结论1:直接包装厂家第三方库复杂度过高,死胡同

自己封装

在前期用C++写的时候,做了一个名为reader的类进行封装,将各种数组和结构体操作转化为标准的C++类。继续SWIG:

%module reader
%include <std_string.i>
%include <std_vector.i>
%include <std_pair.i>
%{
#define SWIG_FILE_WITH_INIT
#include "A.h"
#include "reader.h"
%}
%include <windows.i>
%include "reader.h"
%template(RecordVector) std::vector<guardian::read_data>;
%template(EventVector) std::vector<guardian::event_data>;

由于我在reader.h种使用了标准库,而且很多方法需要既返回操作是否成功,又要返回结果,使用了返回值pair以及tuple,现在看来是一种很失败的设计。但是就传统的C/C++的设计方法,返回的结果直接作为引用传入参数,这种方式又是Python极度不同的。

继续编译,我又碰到了坑:

  • 采用的vectorpair几乎无法使用,返回值无法获取

结论2:SWIG对于C++新特性,特别是C++11的新特性支持不足,需要大量的类似于%template的定义告知SWIG内部的数据结构,过于繁琐

坑二·CPython&Cython

需要说明的是,CPythonCython不是一个东西!

CPython是一种应用最广的Python实现,可以使用非常多的第三方库,与之对应的是JPythonIronPython。使用Python官方文档里的C/C++ API所说的方式进行扩展。
Cython是一套类似于C语言的用于扩展Python的方法,将第三方库进行接口描述,从而进行编译。

经过查看CPython的文档,我决定放弃这个想法

结论3:原生CPython的扩展开发不适用于快速开发

坑三·Boost.Python

人生苦短,我用Python
人生苦短,我用Boost
人生苦短,我用Boost.Python
——沃·兹基朔德

Boost是个好东西,由于一直被人告知“编译繁琐”我一直敬而远之,这次只能硬着头皮上了。

官网,下载最新版本的Boost

确认本机当前Python的编译器版本,输入

D:\> python
Python 2.7.5 (default, May 15 2013, 22:43:36) [MSC v.1500 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>>

显示编译器是MSC v1500,即VS2015的编译器,其他版本请自行Google之。

解压Boost后,先进行bootstrap,编译自带的编译系统,这个时候需要VS的编译环境,从开始菜单种找到VS2015 开发人员命令提示,从这里进入后会运行VS的vcvarsall.bat脚本,设置VS的编译环境变量和搜索路径。

运行bootstrap.bat,会将boost自己的编译系统bjamb2)编译。由于我这里只需要Boost.Python,编译时我只编译了它。

b2 stage --toolset=msvc-14.0 --with-python --stagedir="X:\boost\boost_1_61\vc14" link=static runtime-link=shared runtime-link=static threading=multi debug release
b2 stage --toolset=msvc-14.0 --with-python --stagedir="X:\boost\boost_1_61\vc14" link=shared runtime-link=shared runtime-link=static threading=multi debug release

可以看到一共编译了8个结果,分别是Debug/Release,RuntimeLink的Static/Shared,Link的Static/Shared的排列组合。

(此处应有图)

改写reader.h

改写reader.h,将库中返回值都修改为boost::python::dict,这样返回值就是纯正的python字典对象了,可以随意使用json等进行序列化,方便其他库使用。

题外话

使用Boost.Python可以在C++种使用一种近似于Python的方法写代码,比如在我代码内部有对传入的时间转换为std::time_t,在python种还要进行转换,我直接做了如下转换,将其转换为datetime.datetime对象:

class pyutil
{
public:
boost::python::object datetime;
pyutil()
{
init_datetime();
}
private:
void init_datetime()
{
using namespace boost::python;
object dt = import("datetime");
this->datetime = dt.attr("datetime");
}
}
pyutil gUtil;
boot::python::dict guardian::guardian_reader::getSomeData()
{
boost::python::dict res;
// 其他代码
res["eventTime"] = gUtil.datetime.attr("fromtimestamp")((int)std::mktime(&tm));
return res;
}

编译reader

在VS2015中,新建Win32工程,选择DLL空项目,将需要的代码都添加至项目内:

头文件:

  • A.h
  • reader.h
  • common.h(类似于stdafx.h,有很多公共windows和STL头文件)

源文件:

  • reader.cpp
  • export.cpp

这里的export.cpp是作为导出reader.cpp种内容的,大致如下:

#include <boost\python.hpp>
#include "common.h"
#include "A.h"
#include "reader.h"
// 建议:将lib都放在源代码中,编译时可以不用指定需要链接的库
#pragma comment(lib, "A.lib")
#pragma comment(lib, "python27.lib")
#ifdef _DEBUG
#pragma comment(lib, "boost_python-vc1400-mt-gd-1_61.lib")
#else
#pragma comment(lib, "boost_python-vc1400-mt-1_61.lib")
#endif
// 注意,这里的模块名称一定要与工程名称一致
BOOST_PYTHON_MODULE(pyreader)
{
using namespace boost::python;
enum_<operation_result>("operation_result")
.value("unknown", operation_result::unknown);
class_<guardian_reader>("Reader", init<std::string, std::string, std::string, std::string>())
.def("connect", &guardian_reader::connect)
//定义需要暴露的方法
.def("close", &guardian_reader::close);
}

在工程属性 => C++目录 => 包含目录种加入:

  • boost目录
  • python的include目录

库目录加入:

  • boost最终的编译库目录,比如`X:\boost\vc14\libs
  • python的libs目录

将常规种的“目标文件扩展名”改为.pyd编译即可。

python的setuptools编译

上一种用VS编译后的pyd,直接放在当前目录下是可以使用的(当然要配合boost.python的dll),但是不能作为一个包,在诸如PyCharm的环境中,是看不到pyreader库存在的,现在用另一种方法编译。

在pyreader的vs项目下(或者单独的目录也行)新建一个setup.py文件

from setuptools import setup, Extension
setup(
name='pyreader',
version='1.0.0',
description='A hardware reader wrap for Python',
author='Xana Hopper'
author_email='xanahopper#163.com',
include_package_data=True,
zip_safe=False,
ext_modules=[
Extension('pyreader', [
'Reader/guardian_reader.cpp',
'Reader/export.cpp'],
include_dirs=[
'Reader/',
r'D:\Program Files (x86)\WinPython-32bit-2.7.5.3\python-2.7.5\include',
r'D:\Lib\Boost\boost_1_61_0'
],
library_dirs=[
'Reader/',
r'D:\Program Files (x86)\WinPython-32bit-2.7.5.3\python-2.7.5\libs',
r'D:\Lib\Boost\boost_1_61_0\vc14\lib'
],
libraries=[],
extra_compile_args=['/EHsc']
)
]
)

需要特别说明的是,最后的extra_compile_args=['/EHsc']一定要加!否则链接时一定会出错!

进入VS2015 开发人员命令提示,将当前版本的D:\Program Files (x86)\Visual Stuio 14.0\VC\bin加入当前PATH

set path=D:\Program Files (x86)\Visual Stuio 14.0\VC\bin;%PATH%

设置环境,由于setuptools默认使用VC9环境,所以要进行以下设置:

SET VS90COMNTOOLS=%VS140COMNTOOLS%

这个时候,就可以进行编译了。输入

python setup.py build

如果编译没出错,就可以进行安装了

python setup.py install

到此,所有坑都爬完了。

结尾语

一个周末都在爬坑,感谢以下链接的作者,真的是救人于水火之中:

以及若干未记录的网页。

另,目前还没找到静态链接Boost.Python的办法,我还在pyreader.pyd的边上放着boost.python的dll,有谁能解救我……