一、文件:手写了三个文件:
1. add_function.h:
float add_function(float, float);
2. add_function.c:
float add(float a, float b){
return a+b;
}
3. add_function.i:
/* file: add_function.i */
// assgin the module name, which will be called in python. for example, import arith
%module arith
%{
/* Everything in this block will be copied in the wrap file. we need "add_function.h" to use the function "add" in add_function.c, so we need to include the head file.
#include "add_function.h"
%}
// list all the functions to be interfaced.
float add(float a, float b);
//or else, we can use
//%include "add_function.h"
//to automatically include all the functions declared in "add_function.h"
为了方便,三个文件放到了同一个文件夹。
二、 步骤
1. 如果没有安装vs,那么,下载一个专门为PYTHON编译C、C++代码的VS工具,其实就是VS用于编译C和C++代码的命令以及windows下的库。有了vs,可以用界面的方式来编译,而使用这套工具,则可以只用命令行来进行编译。
https://www.microsoft.com/en-us/download/details.aspx?id=44266
Microsoft Visual C++ Compiler for Python 2.7
这个工具仅用于python 2.7。下载解压后,里边既有x86的库,也有x64的库。使用vcvarsall.bat来设置环境变量,使得你的程序可以方便的找到相应的包含文件和库。
注意,如果是x86(32位的操作系统),在命令行运行命令: vcvarsall.bat x86;如果是x64(64位的操作系统),在命令行运行命令:vcvarsall.bat x64
2. 我们还需要下载一个swig工具。
http://www.swig.org/download.html
请下载windows专用的包,直接解压得到预编译好的exe文件。包的名字类似:swigwin-版本号。
然后,解压该包,将其中swig.exe所在的目录加入到环境变量path中去,这样可以在任意工作目录调用swig.exe。
3. 搭建好环境,安装好swig工具之后,我们开始正式的生成可用于python调用的库pyd。
这第一步就是手写一个.i文件,这里就是上边提供的add_function.i文件。主要包含三个基础的部分:
1. 要用于导入的module的名称,格式为 %module name,这样以后在python中import name即可使用
2. 列举编译源C文件,也就是add_function.c文件需要的头文件。写在%{与%}之间,这一段要被拷贝到新的wrap.c中,所以直接写C语言的格式,也即是#include "XX.h"或者#include
3. 列举(声明)所有要放到库中的api函数。写法与在头文件中声明一个函数是一样的。或者使用%include "XX.h",直接导入这个头文件中声明的所有函数和变量。
4. 复杂的用法请参考swig的文档。
使用的命令是:swig -python add_function.i
1. -python指的是要生成用于python的包。
2. 对于C++代码,还要加上选项-c++。
3. 具体swig的使用细节可以用命令swig -help来查看。
这样,我们生成了一个叫做add_function_wrap.c的文件,和一个arith.py的文件。
1. add_function_wrap.c这个文件,就是用swig自动将add_function.c转换为符合python生成库要求的C代码的格式。
2. arith.py则是以后要import的文件,这是个接口文件。
4. 编译
把所有C文件编译成目标文件obj。
1. 编译所有用到的C文件!这里,我们使用了两个C文件,分别是add_function.c和add_function_wrap.c。
2. 把每个C文件包含的头文件所在的文件夹也要指定出来,用/I参数来指定。由于add_function_wrap.c使用了Python.h这个文件,所以要把其所在文件夹导入进来。add_function.h也要用到,但是其在当前文件夹内,所以就不用专门去导入了。
3. 这一步,我们只编译,并不进行链接操作,所以要加上/c。之所以这一步不链接,因为链接需要依赖其他库,一步来完成,容易出错,影响情绪。。
使用的命令是:cl /c add_function.c add_function_wrap.c "/IC:\Program Files\Anaconda2" "/IC:\Program Files\Anaconda2\include"
1. 我安装的是anaconda,所以Python.h在anaconda中,如果安装的其他python2.7的包,请查找Python.h这个文件所在的文件夹,然后将该文件夹导入进去。
2. 因为路径中有空格,所以用双引号引住。
这样,生成两个目标文件,分别是add_function.obj和add_function_wrap.obj。
5. 链接obj到pyd。
链接obj,需要把所有依赖的obj和lib文件都找出来,放到一起链接。这里,我们只用到了一个额外的库,那就是python27.lib。
命令为: cl /LD add_function.obj add_function_wrap.obj "C:\Program Files\Anaconda2\libs\python27.lib" -o _arith.pyd
1. /LD 表示生成动态库(?)。具体含义请参考cl -help
2. python27.lib文件名中有空格,所以用双引号围住。
3. 注意,生成的pyd文件要用_arith。因为在arith.py中,默认的名字就是这个。
这样,我们就生成了_arith.pyd文件。
6. 测试
我们在当前文件夹下打开cmd,然后输入python,回车,进入python的命令界面。
输入import arith
成功!
输入dir(arith)
可以看到,我们的add函数已经在其中啦!
7. 打包到本机环境中
1. 在6中,我们的命令行是在当前文件夹下,所以可以直接import arith。但是,我们如果要把这个模块放入到一个包里边来发布给其他人使用,或者,把这个包安装到我们本机环境,就像用pip安装的其他包那样来使用的话,我们得做个安装文件,也就是setup.py。
################################setup.py#################################################
# -*- coding:utf8 -*-
# 注意,该文件的编码格式设置为utf8。可以用notepad++这个编辑器来修改文件编码格式
# 从distutils.core这个模块里导出setup这个函数
from distutils.core import setup
setup(
name='arith', # 必需:这个package的名字,指的是如果用sdist命令来打包成zip等压缩文件的话,压缩文件的名字。
version='0.0.1', # 非必需:版本号
description='Cool short description', # 非必需:一句话的描述,不能包含段落。
author='Author', # 非必需:作者名字
author_email='[email protected]', # 非必需:作者的邮箱地址
url='repo.com', # 非必需:该安装包的网页地址
packages=['mx_arith'], # 必需:要打包的package。这个package指的是python的package,即,下边是有__init__.py文件的。比如,os和os.path,这样的。os.path是一个独立的package,并不在os之下,但是文件夹结构上,path在os文件夹下。我们的code放在mx_airth这个文件夹下,所以我们只有一个package,那就是"mx_arith"。
long_description=README, # 非必需:详细的文档说明,可以包含多个段落。
include_package_data=True, # 看情况:package里是否包含额外的数据要打包进去。此次,我们要把编译的_arith.pyd文件也放到package mx_arith里,所以这个选项要选择True。
package_data={"mx_arith":["_arith.pyd"]}, # 看情况:上边一个选项说明我们有额外的数据,这个选项则用于指定,把哪些数据,打包到哪些package里。这是个字典,key是package的名字,value是要放到这个package的所有文件的列表,一般是非py的文件,因为py的文件,会被自动打包进去的。
classifiers=[ # 非必须:这个选项,不清楚用途,貌似可以不用
# Trove classifiers
# The full list is here: https://pypi.python.org/pypi?%3Aaction=list_classifiers
'Development Status :: 3 - Alpha',
]
)
#################################################################################################
2. 光有setup.py还不行,还要注意一下文件夹结构。对于当前项目,我们的文件夹结构是这样的:
--test_setup
--MANIFEST.in
--README.txt
--setup.py
--mx_arith
--__init__.py
--arith.py
--_arith.pyd
1) MANIFEST.in:文件名不要错。这个文件的作用是,指定把哪些非python的文件打包到package的压缩包里。我们要把_arith.pyd打包到压缩包里,所以,在该文件中,我们用了一行:recursive-include mx_arith *.pyd。表示把mx_arith文件夹下的所有pyd文件都打包到pacakge mx_arith里。关于MANIFEST.in的详细用法,参考python doc。
2) README.txt:文件名不要错。这个文件的作用是setup中指定的long_description的来源文件。
3) setup.py:这个必须有。
4) 文件夹mx_arith,包含用于import的文件arith.py和库_arith.pyd。另外要有__init__.py,表明这是个package。__init__.py文件里内容可以为空。我们实际调用的是_arith.pyd里的核心代码,arith.py是swig生成的包装文件。
3. 接下来是几个命令:
1. 安装到本地电脑,在setup.py所在文件夹打开命令行,运行python setup.py install。那么会把mx_arith文件夹拷贝到Python安装目录下的site-packages文件夹下。这个文件夹是import默认的查找package和module的地方。这样,你可以在本机随时随地import mx_arith,import mx_arith.arith或者from mx_arith import arith了。
2. 打包,然后供给别人使用(不确定,因为有我们本机编译的_pyd文件,别人的电脑可能用不了)。使用命令python setup.py sdist。这样会把相关数据打包到.\dist文件夹下。文件名正是我们指定的arith-0.0.1.zip,后边的是版本号。
8. 摸索中出现的问题,原因,和解决办法:
1. 使用cl命令编译C或者C++文件时,会出现unresolved external symbol XXXX:
这是因为你的代码里调用了XXXX这个变量或者函数,但是却无法在编译好的文件中找到这个变量或者函数的定义。这其实是个链接错误。对于此次打包,发生的原因可能有三个:
1) 你在编译C或者C++代码时,没有用/c或者-c参数,导致cl同时会进行链接操作。
2) 你在编译C或者C++代码时,没有用/I参数来指定头文件所在的文件夹。
3) 你在链接obj文件时,没有把相关的库放到一起,比如python27.lib等。
2. 找不到cl命令的错误。这个是因为你没有把cl所在的文件夹放入到环境变量path中。这里,你应该是忘记或者没有准确的运行vcvarsall.bat文件来设置windows vs编译环境。
3. 找不到LIBXXX.lib等lib文件。同样,你没有准确的运行vcvarsall.bat来设置相关环境变量。注意,vcvarsall.bat的运行环境是它被安装的文件夹下的命令行,如果拷贝它到其他地方,再运行,肯定出错,因为里边的路径是相对路径,那样的话你需要修改bat文件。或者,你就在bat文件下的命令行运行这个bat,然后再cd到其他路径即可。
三、基础知识
C语言在windows下的编译过程:我们写的源代码,一般是.h或者.c文件。.h是头文件,主要是声明了一些变量和函数,也可以在.h中定义,但是不推荐。注意,声明,表示告诉系统,我们有这么个变量或者函数。但是,具体这个函数是怎么实现的,也就是函数的定义,一般在与.h文件同名的c文件中。这两个文件可以在同一个文件夹,也可以不在一个文件夹。
我们编译源代码,一般是生成两类文件,一种是可运行的exe文件,一种是可复用的lib或者dll库文件。编译exe文件,那么在某个c文件中,必须要有main函数。编译dll和lib文件则不需要有main函数。
编译的简略流程是,首先把c文件编译成二进制的目标文件,即obj文件,这应该是汇编的指令,忘记了。这个过程中,需要告诉编译器以下信息,你要编译的C文件的路径,编译这个C文件所需要的头文件所在的文件夹。编译这个C文件,主要是把其中的函数和变量转为二进制代码,对于其中包含的头文件以及其中所声明的函数,暂时用引用的方式留在代码里,并没有真正的填充。但是,有了头文件的路径,我们就知道引用的函数的一些基本信息,比如它的返回值类型,它的参数类型,它的名称,等等,可以用于初步的检查,函数的调用是否准确。所有被用到的C文件,都要被编译成obj文件。
把所有相关的C文件都编译好之后,就是进行链接操作。需要提供所有要被链接在一起的obj文件的路径,包括系统已经提供的库,即lib和dll的路径。因为lib和dll就是被打包好的obj文件。如果找不到某个函数所在的obj文件,或者找不到引用的库文件,那么就会报错,或者说有 unresolved external symbol,或者说没有找到 XXX.lib库。
有用的参考,大多是youtube上的视频教程:
1. https://www.youtube.com/watch?v=J-iVTLp6M9I
该网页是youtube中,从原始C到符合python导入格式的C,再到使用swig工具来将原始C生成为符合python导入的C的教程。从这个简单的例子中,可以充分了解这个过程。
2. https://www.youtube.com/watch?v=y_eh00oE5rI
Writing a C++ extension for Python 2.7 with Visual Studio
这个教程则详细介绍了使用vs来写用于python 2.7的C++代码,然后编译成可用于python的库pyd的过程。作者故意把各种可能遇到的错误都显示出来,并且给出了元音和解决方法。他加深了我理解C++编译对包含头文件,库的设置。这个教程还有个下集,更加复杂的项目和工程。
https://www.youtube.com/watch?v=IRE67QFYu4s
C++ extensions for Python with Visual Studio, part 2
3. https://docs.python.org/2/
python 2.7的doc文档。最后的最后,还是要从文档中查基础的使用方法。
最后:感谢youtube,这是个好平台!希望国内的视频网站也能多一些这种教程,方便人们知识的传递和学习!