淺談python中使用C/C++:ctypes

MamieEads 8年前發布 | 35K 次閱讀 Python Python開發 C/C++

前言

python 這門語言,憑借著其極高的易學易用易讀性和豐富的擴展帶來的學習友好性和項目友好性,近年來迅速成為了越來越多的人們的首選。然而一旦拿python與傳統的編程語言(C/C++)如來比較的話,人們往往會想到效率問題。本文不打算探討語言之間的比較,然而python實際使用時確實會有能用更底層的C/C++更好的情況,因此本系列旨在介紹幾種相對常見的 python環境下調用C/C++ 的方法。(挖坑:CTYPES,SWIG,BOOST.PYTHON,CYTHON)

閱讀這篇文章需要什么?

語言:簡單的python基礎與簡單的C/C++基礎。

C/C++的環境與python的環境。

步步跟進

搜索引擎/工具書 隨時查詢不明白的地方。

PS:本文中會有一些延伸性的知識點,加之本人語文水平慘不忍睹導致文風驚悚,所以 如果閱讀途中感到不適請務必跳過延伸性的部分(用大括號括起來的部分)

目錄

  • 一、環境配置

  • 二、C/C++一側

    • 代碼

      • extern "C"

      • extern 和 static

      • #ifdef

      • DLL_EXPORT

      • __cdecl和__stdcall

  • 三、CTYPES

    • 加載

    • 數據類型

    • 訪問導出變量

    • 函數進出參數的定義

      • argtypes

      • restype

    • 指針和引用

    • 數組

    • 小結

  • 四、參考資料

一、環境配置

介于這是本系列的第一篇,我簡單介紹一下環境(vim+命令行的大佬們可以跳了……):

python:可以從[python官網]( https://www. python.org )下載(不KX上網好像有點難開……?)。現在選擇2還是3這個問題……依然是各有說法。然而可以兩個都裝因此也無所謂啦。IDE則是python默認的。PS:要注意一定要和C那邊的位數匹配!如果GCC是32位的,用64位python就會報錯,血的教訓……

C/C++:我用的IDE是[codeblock]( Code::Blocks )——一個跨平臺的開源C/C++集成開發環境。建議下載mingw-setup版,自帶GCC/G++和GDB debugger。另外如果需要VC編譯的話還需要下載VC并且在codeblock的compiler settings里面調整路徑。

現在作為一個python高手和C高手的李狗蛋從python官網上下載了python3.5 32-bit和codeblock mingw-setup,他胸有成竹,因為他明白他的命運,他是宇宙中排名unsigned int(-1)的程序員,是超越最強的男人(并不)。他興奮的準備寫出一個可以征服世界的程序。

二、C/C++一側

ctypes的使用是通過調用C/C++的動態鏈接庫(DLL)實現的,因此在進入正題之前,還要先講講動態鏈接庫的構建方式。這一塊會牽扯到各種編譯器和系統和語言相關的問題,本文只討論我們目前所需要了解的部分。

庫的本質 就是一個打包好的代碼包,一般分為靜態(.lib .a)和動態(.dll .so)。靜態庫在主程序編譯時就會被一并編譯到最終的可執行文件中,然而python并沒有編譯這個過程,python主要使用的是動態庫,即在運行時再去庫里找內容。

對于使用IDE的人來說,要建一個庫只需要在新建項目時選擇項目屬性為庫即可。

{

而對于命令行的使用者,則需要在編譯時添加一些命令(以C++為例,C把g++改成gcc即可):

g++ -fPIC -shared -o libsource.so Source.cpp

其中-shared代表這是動態庫,-fPIC使得位置獨立,如果程序本來就是獨立的話會有警告,無視即可) -o指定了輸出文件,改成dll后綴一樣可以用。

}

如果要使用C/C++調用這個庫則還需要許多繁瑣的流程,好在我們是用python調用,所以可以到此為止。

李狗蛋看了以后,運用他極高的編程技術,穩定的操控著鼠標,依次點擊了code:block菜單欄上的File-New-project-Dynamic Link Library,并且一路按著下一步,動作有如雷霆一般,好似忘我的舞者。忽然,他發現對話框卡在了一個地方,原來是要給程序命名。他略微沉思了一會,就毅然打下了一行英文:SSR 似乎是為了彌補生活中的空缺。

代碼

彈出來的窗口里充斥著許多不知名的符號,李狗蛋吃了一驚,心里道:呵!有意思,居然讓本大爺愣了1s,好漢不吃眼前虧,我先來學習一個。

假如你們像我說的那樣在IDE中新建了一個動態庫project,你們多多少少會被蹦出來的樣板文件鬧得有點不知所措(本文假設讀者是和我一樣的只寫過十分簡單的C/C++文件的人)。這不奇怪,為了各種方面的兼容性,一個文件如果要按照項目的標準來寫,會有一些個人獨自寫小文件時用不上的命令。比如:

extern "C"

由于C++多了函數重載的功能,導致了一個函數不能僅僅用它的名字來確定。

其結果就是編譯以后函數名(如`git`)往往會變成像`_Z3gitv`這種樣子(不同編譯器變法還有些不同)。那導致的直接原因就是難以調用。而這就到了`extern "C"`命令出場的時候了,該命令告訴編譯器用C的方式編譯這一句,而C的函數名不會改變,于是編譯后可以直接用函數名調用。對我們來說幾乎每一個C++的函數都要加上extern "C"。

/*函數重載:比如函數 int fun(int a)和int fun(double a),在C的眼里是同一個函數(名字一樣),在C++眼里是不同的函數(名一樣,姓氏,性別,字號不一樣啊)。理論上C++的更加科學,但是為了區分,它會對函數進行重命名。比如對李狗蛋:LJ_李狗蛋_男_84_82_86_單身,這樣的結果就是我說李狗蛋,它不知道是誰,只知道有個名字很長的函數。所以我們需要extern "C"來告訴它不要亂給別人改名,會被揍的。*/

extern和static

看到標題很多人都會想:這里的extern和上文有關系嗎?……

答案是 有,也沒有。要解釋的話我們就要簡單了解一下這兩個關鍵詞了。

{

static對于局部變量來說意為靜態變量,即不隨該函數的生死而產生或消亡。當然這是題外話了,我們要談的是另一方面的static。

}

對于 全局變量/函數定義 來說,extern和static是一對 反義詞

extern指示內容不僅僅限于本文件,可能在別的文件里被定義/被調用,而static則表示該元素僅僅允許在本文件中使用。上面命令(extern "C")的本質是“ 是extern的,同時是C的 ”。

{

然而由于"C"事實上只有這一種用法,而且只有函數需要被外部調用時我們才關注它是否被改名了,所以事實上形成了一種類似英語中的固定搭配的用法,又沒有關系了。

}

在我們的使用方面,可以用static來形成類的private部分的效果。

/* 也就是說,假如我們寫了一個函數叫getMyDarkDragonEyeOn(),但我們不想被外部調用,那我們就用static聲明這個東西我要偷偷藏起來不給別人用。但是如果別人調用一個extern的函數ZhongErSoul() ,這個函數是可以調用static函數的(因為他是內部)。變量同理,我們可以聲明static finalexamScore來避免被直接訪問到不想被訪問的變量。*/

DLL_EXPORT(如果用VC編譯必讀)

對于GCC編譯來說,所有函數默認(未加static)都是導出的,即可以被外部調用的。然而對于VC來說就不一樣了, VC默認不導出 ,所以我們需要這樣一段:

#ifdef _MSC_VER
        #define DLL_EXPORT __declspec(dllexport) 
    #else
        #define DLL_EXPORT

其中_MSC_VER 是VC的一個宏定義,從而檢測編譯器是否是VC而決定是否使用__declspec( dllexport )

然后這樣寫函數

extern "C" DLL_EXPORT int function()

{

cdecl與__stdcall

這是兩種不同的調用方式,涉及到編譯和底層匯編的一些細節,這里不作展開。C/C++默認的都是使用__cdecl,可通過在函數前面添加關鍵詞定義該函數的調用方式,我們可以不用管。

#ifdef

許多這種類似的語句為宏,一般樣板里的都是為了避免重復定義而存在。意為“如果定義了……那么……”,類似的還有一些,可以自行了解。

}

對于我們來說,(如果用GCC)只要在寫出來的文件里加extern "C"(可能還要static)就夠了。

讀完以后,李狗蛋渾身充滿了能量,他打算在這個項目里祭出他最強的一段代碼,以感謝文章的作者。于是他寫了這么一段:

#include<cstdio>
extern "C" void saikyo()
{
     printf("Hello world!");
}

手起刀落,狗蛋滿意的笑了。

三、CTYPES

加載

首先毫無疑問 我們要import ctypes。

ctypes有CDLL和WinDLL兩種調用方式,對應上面說過的__cdecl和__stdcall,我們一般使用CDLL。

windows下會自動補充.dll后綴,而Linux則需要包含擴展名在內的全名才可調用

要加載一個dll,我們有許多方法:

cdll.filename
cdll.LoadLibrary("filename")
CDLL("filename")

這三個都會調用filename.dll(Windows下自動補充后綴)并返回一個句柄一樣的東西,我們便可以通過該句柄去調用該庫中的函數。如:

cdll.study.math()

也可以把句柄保存下來使用:

h = cdll.study
    h.math()

{

這里還有一個用法,由于dll導出的時候會有一個exports表,上面記錄了哪些函數是導出函數,同時這個表里也有函數的序號,因此我們可以這樣來訪問表里的第一個函數

cdll.study[1]

(要注意是從1而非0開始編號) 返回的是該函數的指針一樣的東西,如果我們要調用的話就在后面加(parameter)即可。

關于exports表,GCC似乎編譯時會自動生成def,可以在里面查,如果只有DLL的話,可以用VC的depends,或者dumpbin來查。

}

數據類型

李狗蛋在dll文件所在的目錄下建了一個py文件,然后用IDLE打開并run python shell,反復使用上一小節所學的的技能輸出了幾十行Hello World,旁邊的張小花不由得露出了心心眼。一旁的王小錘卻嗤笑一聲:“有什么好囂張的,它能做數學題嗎?”李狗蛋聽完心里不由得一股怒火,居然敢挑戰狗王的權威,今天就讓你看看什么叫老司機。于是他又加了一個函數,還不忘加上新學的extern "C":

extern "C" int hardestproblem(int a,int b)
{
     return a+b;
}

/*None, integers, bytes objects and (unicode) strings are the only native Python objects that can directly be used as parameters in these function calls. None is passed as a C NULL pointer, bytes objects and strings are passed as pointer to the memory block that contains their data (char * or wchar_t *). Python integers are passed as the platforms default C int type, their value is masked to fit into the C type.*/

上面的摘自python官方文檔。描述了幾種 可以直接傳遞 的參數類型及其在C一側的形式。

狗蛋在python里輸入了以下內容

from ctypes import *
god = cdll.god
god.hardestproblem(1,2)

得到了輸出3,完美的解決了小錘的問題。

對于我們來說,再加上浮點值幾乎就夠一般使用了。然而浮點值不是默認可傳遞的,于是我們需要進行類型轉換:

c_float(3.14)#單精度浮點類型
    c_double(3.14)#雙精度浮點類型

定義變量后,用i.value訪問值。

小錘似乎還想說些什么,然而早已被狗蛋看穿:“你想讓我求圓的面積吧,哼哼。你以為問個這么尖端的問題我就不會答了?看著。于是在C++源文件中加了一個函數:

extern "C" double circle(double pi,double r)
{
      return r*r*pi;
}

運用剛剛學的轉換語句,他在python里打道:

god.circle(c_double(3.14),c_double(1))

然而似乎命運總是會捉弄主角一下,屏幕顯示出了18805556,狗蛋的臉漸漸變得鐵青……

狗蛋遇到的問題先放放,單從參數類型來看,似乎這樣就夠我們使用了。然而這其中需要注意一下字符串,字符串由于編碼等等會有不少問題,比如:

str與bytes之間的編碼問題,python默認的字符串str是unicode編碼,占用兩個字節。因此如果你只是用雙引號寫一個字符串傳到函數里,如果你函數里又需要修改/讀取指定的位,那么就會發生奇怪的事情,其原因是unicode的兩字節與Char的一字節不匹配。詳細可以參考 字符串和編碼

為了解決這個問題,我們還要加`b""`讓他強制生成bytes類型。

或者更安全的方法是:用create_string_buffer()生成一個更類似與C字符數組的東西以便操作安全。如:

create_string_buffer(b"abcdefg",10)

來開一個長度為10的前面部分是abcdefg后面用NUL補齊的字符數組

另外:C中printf的輸出只會出現在stdout(即命令行里),不會出現在IDLE里。

為了挽回小花的芳心,狗蛋回家后仔細研讀了本教程,寫了一個很浪漫的程序發給小花:

extern "C" int password(char *A,int n)
{
     printf("%c",A[n])
}

然后在讓小花在python中輸入以下內容

from ctypes import *
god = cdll.god
god.password(b"Live",0)
god.password(b"cool",1)
god.password(b"Have",2)
god.password(b"None",3)

結果我們都能夠猜到:小花十分感動…………………………………………然后拒絕了他。

詳細的類型名稱表格見文章后面附圖。

{

如果你需要用自定義的數據類型來當作參數傳遞的話,需要參數中有個名為_as_parameter_的變量,ctypes會去找這個名字的變量來當作參數傳遞。涉及自定義類的內容不作展開。

}

訪問導出變量

和函數一樣,dll中的導出變量也可以被外部訪問。格式如下:

c_int.in_dll(study,"score")

其中c_int表示數據類型,in_dll表示在dll內,study處是dll名,后面的字符串是變量名。要注意變量和函數一樣, 不加extern"C"的話會被編譯器改名

函數的進出參數的定義

argtypes

python并不會直接讀取到dll的源文件,那么如何告訴python,函數需要什么參數呢?答案是用argtypes。

fuction.argtypes = [c_char_p,c_int]

這樣,在后面你使用fuction時python會自動處理你的參數,從而達到像調用python參數一樣。

restype

和上面一個一樣,python不僅看不到函數要什么,也看不到函數返回什么。默認情況下,python認為函數返回了一個C中的int類型。然而如果我們的函數返回別的類型,就需要用到restype命令。

function.restype = c_char_p

指定了fuction這個函數的返回值是c_char_p的,從而讓python在處理時按照我們希望的那樣處理。

我們甚至可以設置函數的返回值為一個python對象(比如函數),目標函數執行后返回int并用該int直接調用該python對象。這里不作展開。

話說狗蛋被拒絕后傷心欲絕,功力大失,遂進網吧修行10年,讀到了這一段,如醍醐灌頂。于是約了小錘午夜時分,情人谷上見。時辰到了,只見小錘騎著他的自行車,后座載著小花晃晃悠悠上山來了。

“小錘!十年前你害得我失去一切的問題,我終于解出來了!我不怕事的python扛把子今天要告訴你,美人,只配強者擁有!

話音未落 ,狗蛋拿出他早已寫好的程序,C部分沒有改動,然而python部分,卻確確實實不一樣了。

circle = god.circle
circle.argtypes = [c_double,c_double]
circle.restype = c_double
circle(3.14,1)

只見屏幕閃耀著金光(狗蛋視角),蹦出了那個令狗蛋渾身一震的數字:3.14。

狗蛋一愣,隨后是止不住的狂喜,他苦苦追求了這么多年,總算有了結果。據凌云上的同學說,半夜聽到有人一直在哈哈哈哈哈哈,大家都準備抄家伙了。

小錘和小花對視數秒,訕訕的說:那……沒事我們先下去了啊……點了雞排。

于是,上來不過數分鐘,兩人又騎著車沖下了山,抱的緊緊的。而狗蛋的笑聲遲遲不散,卻多了幾分凄涼……

{

指針和引用

指針和引用是非常常用的(特別在C中),這里不進行介紹,只講講用法。

byref(i)
    pointer(i)

分別產生i的引用和i的指針,其中如果不必要使用指針的話,引用會更快。可以通過輸出指針來觀察指針的屬性。

數組

ctypes重載了*,因此我們可以用類型*n來表示n個該類型的元素在一起組成一個整體。如定義整數數組類型:

int_10 = c_int *10
    myarr = int_10()
    myarr[2]=c_int(24)

傳到C那邊函數形參就應該是(int a[ ]),多維數組同理。

}

小結

到現在我們應該學會了怎么創建dll,怎么用ctypes加載dll,訪問里面的函數和變量。傳一些基本的參數進去并接收一些返回值。ctypes還有一些關于structure,union等的用法,就不一一闡述了。(其實是我也不怎么會python……)現在學會的內容應該足夠支持起普通的C/C++調用了。

后來有一天在機房,一個頹廢的中年男子走了進來,開了一臺偏僻的機器,問我:ctypes的dll載入有4種寫法,你知道嗎?

----------

(完整的類型對應表格摘錄如下:)(吐槽知乎居然不能用markdown……)

----------

四、參考資料

1、extern "C"的用法解析 - Rollen Holt - 博客園: http://www. cnblogs.com/rollenholt/ archive/2012/03/20/2409046.html

2、C++靜態庫與動態庫 - 吳秦 - 博客園: http://www. cnblogs.com/skynet/p/33 72855.html

3、__stdcall,__cdecl,__fastcall的區別 - 學無止境 - 博客頻道 - http:// CSDN.NET http:// blog.csdn.net/kiki113/a rticle/details/4971886

4、聊聊Python ctypes 模塊 - 蛇之魅惑 - 知乎專欄: https:// zhuanlan.zhihu.com/p/20 152309?columnSlug=python-dev

5、python 3 documentation: https://docs.python.org/3/library/ctypes.htmlshi

6、字符串和編碼 http://www. liaoxuefeng.com/wiki/00 14316089557264a6b348958f449949df42a6d3a2e542c000/001431664106267f12e9bef7ee14cf6a8776a479bdec9b9000

 

來自:https://zhuanlan.zhihu.com/p/23372572

 

 本文由用戶 MamieEads 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!