使用swig包裝C或C++源代碼在windows下用命令行編譯並用distutils打包供python使用

一、文件:手寫了三個文件:
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 <XX.h>
    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,這是個好平臺!希望國內的視頻網站也能多一些這種教程,方便人們知識的傳遞和學習!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章