楔子 引入 C 源文件我們已經(jīng)知道該怎么做了,,但如果引入的不是源文件,而是已經(jīng)存在的靜態(tài)庫或者動態(tài)庫該怎么辦呢,?C 語言發(fā)展到現(xiàn)在已經(jīng)擁有非常多成熟的庫了,,我們可以直接拿來用,這些庫可以是靜態(tài)庫,、也可以是動態(tài)庫,,這個時候 Cython 要如何和它們勾搭在一起呢? 要搞清楚這一點,,我們需要先了解靜態(tài)庫和動態(tài)庫,。并且一個 C 源文件可以被編譯成一個可執(zhí)行文件,那么我們還要先搞清楚在將 C 源文件編譯成可執(zhí)行文件的時候,,靜態(tài)庫和動態(tài)庫是如何起作用的,。所以暫時不提 Cython,先來說一下靜態(tài)庫和動態(tài)庫,。 C 的編譯過程 假設(shè)有一個 C 源文件 main.c,,只需要通過 gcc main.c -o main.exe 即可編譯生成可執(zhí)行文件( 如果只寫 gcc main.c,那么 Windows 上會默認(rèn)生成 a.exe,、Linux 上會默認(rèn)生成 a.out ),,但是這一步可以拆解成如下步驟:
從 C 源文件到可執(zhí)行文件會經(jīng)歷以上幾步,,不過我們一般都會將這幾步組合起來,,整體稱之為編譯。比如我們常說,,將某個源文件編譯成可執(zhí)行文件,。 而靜態(tài)庫和動態(tài)庫是在鏈接這一步發(fā)生的,比如我們在 main.c 中引入了 stdio.h 這個頭文件,,里面的函數(shù)( 比如 printf )不是我們自己實現(xiàn)的,,所以在編譯成可執(zhí)行文件的時候還需要將其鏈接進(jìn)去。 所以靜態(tài)庫和動態(tài)庫的作用都是一樣的,,都是和匯編生成的目標(biāo)文件( .o 文件)攪和在一起,,共同組合生成可執(zhí)行文件。那么它們之間有什么區(qū)別呢,?下面就來介紹一下,。
靜態(tài)庫 一個靜態(tài)庫可以簡單看成是一組目標(biāo)文件的集合,,也就是多個目標(biāo)文件經(jīng)過壓縮打包之后形成的文件,。而靜態(tài)庫最大的特點就是一旦鏈接成功,那么就可以刪掉了,,因為它已經(jīng)鏈接到生成的可執(zhí)行文件中了,。所以從側(cè)面也可以看出使用靜態(tài)庫會比較浪費空間和資源,,說白了就是生成的可執(zhí)行文件會比較大,因為里面還包含了靜態(tài)庫,。 而在 Linux 中靜態(tài)庫是有命名規(guī)范的,,必須以 lib 開頭、.a 結(jié)尾,。假設(shè)你想生成一個名為 hello 的靜態(tài)庫,,那么它的文件名就必須是 libhello.a,這是一個規(guī)范,。 在 Linux 中生成靜態(tài)庫的方式如下:
我們來做一個測試,首先是編寫一個 C 文件 test.c,,里面內(nèi)容如下:
執(zhí)行命令:
此時 libtest.a 就成功生成了,,然后我們再來編寫一個 main.c 直接調(diào)用:
我們看到這里只是聲明了 sum,但是具體實現(xiàn)則沒有編寫,,因為它已經(jīng)在 libtest.a 中實現(xiàn)了,,我們只需要在使用 gcc 編譯的時候指定即可。
可以看到執(zhí)行成功了,,打印結(jié)果也是正確的,,但這里需要解釋一下里面的參數(shù)。首先 gcc main.c 無需解釋,,表示對 main.c 文件進(jìn)行編譯,。而結(jié)尾的 -o main 也無需解釋,表示生成的可執(zhí)行文件的文件名叫 main,。 中間的 -L . 表示追加庫文件的搜索路徑,,因為 gcc 在尋找?guī)斓臅r候,只會從標(biāo)準(zhǔn)位置進(jìn)行查找,。而當(dāng)前所在目錄明顯不屬于標(biāo)準(zhǔn)位置,,因此需要通過 -L 參數(shù)將寫好的靜態(tài)庫所在的路徑追加進(jìn)去,libtest.a 位于當(dāng)前目錄,,所以是 -L .,。 然后是 -l test,,首先 -l 表示要鏈接的靜態(tài)庫(也可以是動態(tài)庫,后面會說,,目前就只看靜態(tài)庫即可),,因為當(dāng)前的靜態(tài)庫名字叫做 libtest.a,那么把開頭的 lib 和結(jié)尾的 .a 去掉再和 -l 進(jìn)行組合即可,。 如果我們將靜態(tài)庫改名為 libxxx.a 的話,,那么就需要指定 -l xxx;同理,,要是我們指定的是 -l foo,,那么在鏈接的時候會自動尋找 libfoo.a。所以從這里也能看出,,在 Linux 中創(chuàng)建靜態(tài)庫的時候一定要遵循命名規(guī)范,,以 lib 開頭、.a 結(jié)尾,,否則鏈接是會失敗的,。當(dāng)然追加搜索路徑、鏈接靜態(tài)庫的數(shù)量是沒有限制的,,比如除了 libtest.a 之外還想鏈接 libfoo.a,,那么就指定 -l test -l foo 即可。
同理還有頭文件,,雖然這里沒有涉及到,,但還是需要說一說,因為導(dǎo)入頭文件更常見,。如果想導(dǎo)入的頭文件不在搜索路徑中,,我們在編譯的時候也是需要指定的。假設(shè) main.c 還引入了一個自定義的頭文件,,其位于當(dāng)前目錄下的 header 目錄里,,那么編譯的時候為了讓編譯器能夠找得到,我們需要通過 -I 來追加相應(yīng)的頭文件的搜索路徑: 注:追加頭文件的搜索路徑使用的是大寫的字母 i,,當(dāng)前文章的字體讓人很容易和小寫的字母 l 搞混,因此這里專門畫張圖,,并用一個區(qū)分度比較明顯的字體,。 對于頭文件搜索路徑,、庫文件搜索路徑、引入的靜態(tài)庫的數(shù)量,,都是沒有限制的,,可以指定任意個:-I、-L,、-l,。 動態(tài)庫 通過靜態(tài)庫,我們算是實現(xiàn)了代碼復(fù)用,,而且靜態(tài)庫的使用也比較方便,。那么問題來了,既然有了靜態(tài)庫,,為什么我們還要使用動態(tài)庫呢,? 首先是資源浪費,假設(shè)有一個靜態(tài)庫大小是 1M,,而它被 1000 個可執(zhí)行程序依賴,,那么這個靜態(tài)庫就相當(dāng)于被拷貝了 1000 份,因為靜態(tài)庫是需要被鏈接到可執(zhí)行文件當(dāng)中的,。然后是靜態(tài)庫的更新和部署會帶來麻煩,,假設(shè)靜態(tài)庫更新了,那么所有使用它的應(yīng)用程序都必須重新編譯,、然后發(fā)布給用戶,。即使只改動了一小部分,也要重新編譯生成可執(zhí)行文件,,因為要重新鏈接靜態(tài)庫,。 而動態(tài)庫則不同,動態(tài)庫在鏈接的時候不會將自身的內(nèi)容包含在可執(zhí)行文件中,,而是在程序運行的時候動態(tài)加載,。相當(dāng)于只是告訴可執(zhí)行文件:"你的內(nèi)部會依賴我,但由于我是動態(tài)庫,,因此我不會像靜態(tài)庫一樣被包含在你的內(nèi)部,,而是需要你運行的時候再去查找、加載",。所以多個可執(zhí)行文件可以共享同一個動態(tài)庫,,因此也就避免了空間浪費的問題,并且動態(tài)庫是程序運行時動態(tài)加載的,,我們對動態(tài)庫做一些更新之后可以不用重新編譯生成可執(zhí)行文件,。 有優(yōu)點就自然有缺點,相信都看出來了,,既然是動態(tài)加載,,就意味著即使在編譯成可執(zhí)行文件之后,,依賴的動態(tài)庫也不能丟。和靜態(tài)庫不同,,靜態(tài)庫和最終的可執(zhí)行文件是完全獨立的,,因為在編譯成可執(zhí)行文件的時候靜態(tài)庫的內(nèi)容就已經(jīng)被鏈接在里面了;而動態(tài)庫是要被動態(tài)加載的,,因此它是被可執(zhí)行文件所依賴的,,所以不能丟。 然后我們來生成一下動態(tài)庫,,生成動態(tài)庫要比生成靜態(tài)庫簡單許多:gcc 源文件 -shared -o 動態(tài)庫文件,,還是以之前的 test.c 為例: gcc test.c -shared -o libtest.so 在 Linux 中,動態(tài)庫也具有相同的命名規(guī)范,,只不過它是以 .so 結(jié)尾,。但是你真的不按照這個格式命名也是可以的,只不過在使用 gcc 的時候會找不到相應(yīng)的庫,。因為編譯的時候會按照指定格式去查找?guī)煳募?,所以我們在生成庫文件的時候也要按照相同的格式起名字。
然后使用 gcc 對之前的 test.c 源文件進(jìn)行編譯:
我們看到可執(zhí)行文件成功生成了,這里起名為 main1,。引入動態(tài)庫和引入靜態(tài)庫的方式是一樣的,,因為 -l 既可以鏈接靜態(tài)庫、也可以鏈接動態(tài)庫(要是靜態(tài)庫和動態(tài)庫都有怎么辦,?別急,,后面說,目前只考慮動態(tài)庫),。
但是問題來了,,雖然編譯成功了,但是執(zhí)行的時候卻報錯了,,說找不到這個 libtest.so,,盡管它就在當(dāng)前可執(zhí)行文件所在的目錄下。 原因是可執(zhí)行文件在查找動態(tài)庫的時候也是會從指定的位置進(jìn)行查找的,,而我們當(dāng)前目錄不在搜索范圍內(nèi),。這時候可能有人會好奇,我們不是在編譯的時候通過 -L 參數(shù)將當(dāng)前路徑追加進(jìn)去了嗎,? 答案是動態(tài)庫和靜態(tài)庫不同,,動態(tài)庫在鏈接的時候自身不會被包含在可執(zhí)行文件當(dāng)中,我們指定的 -L . -l test 相當(dāng)于只是在鏈接的時候告訴即將生成的可執(zhí)行文件:"在當(dāng)前目錄下有一個 libtest.so,它將來會是你的依賴,,你趕緊記錄一下",。我們可以通過 ldd 命令查看可執(zhí)行文件依賴的動態(tài)庫:
我們看到 libtest.so 已經(jīng)被記錄下來了,所以鏈接動態(tài)庫時只是記錄了動態(tài)庫的信息,,當(dāng)程序執(zhí)行時再去動態(tài)加載,因此它們會有一個指向,。但我們發(fā)現(xiàn) libtest.so 指向的是 not found,,這是因為動態(tài)庫 libtest.so 不在動態(tài)庫查找路徑中,所以會指向 not found,。 因此我們還需要將當(dāng)前目錄加入到動態(tài)庫查找路徑中,,vim /etc/ld.so.conf,將當(dāng)前目錄( 我這里是 /root )寫在里面,?;蛘咧苯?echo "/root" >> /etc/ld.so.conf,然后執(zhí)行 /sbin/ldconfig 使得修改生效,。 最后再來重新執(zhí)行一下 main1,,看看結(jié)果如何:
可以看到此時成功執(zhí)行了,因此使用動態(tài)庫實際上會比靜態(tài)庫要麻煩一些,,因為靜態(tài)庫在編譯的時候就通過 -L 和 -l 參數(shù)直接把自身鏈接到可執(zhí)行文件中了,。而動態(tài)庫則不是這樣,用大白話來說就是,,它在鏈接的時候并沒有把自身內(nèi)容加入到可執(zhí)行文件中,,而是告訴可執(zhí)行文件自己的信息、然后讓其執(zhí)行時再動態(tài)加載,。但是加載的時候,,為了讓可執(zhí)行文件能加載的到,我們還需要將動態(tài)庫的路徑配置到 /etc/ld.so.conf 中,。
此時 libtest.so 就指向 /root/libtest.so 了,,而不是 not found。雖然麻煩,,但是它更省空間,,因為此時只需要有一份動態(tài)庫,如果可執(zhí)行文件想用的話直接動態(tài)加載即可,。除此之外,,我們說修改了動態(tài)庫之后,原來的可執(zhí)行文件不需要重新編譯:
這里我們將返回的 res 加上一個 1,,然后重新生成動態(tài)庫:
結(jié)果變成了 5051,,并且我們沒有對可執(zhí)行文件做修改,因為動態(tài)庫的內(nèi)容不是嵌入在可執(zhí)行文件中的,而是可執(zhí)行文件執(zhí)行時動態(tài)加載的,。如果是靜態(tài)庫的話,,那么就需要重新編譯生成可執(zhí)行文件了。 同時指定靜態(tài)庫和動態(tài)庫 無論是靜態(tài)庫 libtest.a 還是動態(tài)庫 libtest.so,,在編譯時都是通過 -l test 進(jìn)行鏈接的,。那如果內(nèi)部同時存在 libtest.a 和 libtest.so,-l test 是會去鏈接 libtest.a 還是會去鏈接 libtest.so 呢,?這里可以猜一下,,首先我們上面所有的操作都是在 /root 目錄下進(jìn)行的,而且文件都沒有刪除,。
相信結(jié)果很好猜,,我們介紹靜態(tài)庫的時候已經(jīng)生成了 libtest.a,然后 -l test 找到了 libtest.a 這沒有任何問題,。然后介紹動態(tài)庫的時候又生成了 libtest.so,,但是并沒有刪除當(dāng)前目錄下的 libtest.a,而 -l test 依然會去找 libtest.so,,說明了 -l 會優(yōu)先鏈接動態(tài)庫,。如果當(dāng)前目錄不存在相應(yīng)的動態(tài)庫,才會去尋找靜態(tài)庫,。
我們在 /etc/ld.so.conf 中將當(dāng)前目錄給刪掉了,所以編譯成可執(zhí)行文件之后再執(zhí)行就報錯了,,因為找不到 libtest.so,,證明默認(rèn)加載的確實是動態(tài)庫。 但是問題來了,,如果同時存在靜態(tài)庫和動態(tài)庫,,而我就想鏈接靜態(tài)庫的話該怎么做呢?
通過 -static,,強(qiáng)制讓 gcc 鏈接靜態(tài)庫,。另外,如果執(zhí)行上面的命令報錯了,,提示 /usr/bin/ld: cannot find -lc,,那么執(zhí)行 yum install glibc-static 即可。因為高版本的 Linux 系統(tǒng)下安裝 glibc-devel, glibc 和 gcc-c++ 時不會安裝 libc.a,,而是只安裝libc.so,。所以當(dāng)使用 -static 時,libc.a 不能使用,,因此報錯 "找不到 lc",。 我們執(zhí)行一下:
這里再提一個問題:鏈接 libtest.a 生成的可執(zhí)行文件 和 鏈接 libtest.so 生成的可執(zhí)行文件 哪一個占用的空間更大呢?好吧,,這個問題問的有點幼稚了,,很明顯前者更大,但是究竟大多少呢,?我們來比較一下吧,。
我們看到大小確實差的不是一點半點,再加上靜態(tài)庫是每一個可執(zhí)行文件內(nèi)部都要包含一份,,可想而知空間占用量是多么恐怖??,,所以才要有動態(tài)庫。因此靜態(tài)庫和動態(tài)庫各有優(yōu)缺點,,具體使用哪一種完全由你自己決定,就我個人而言更喜歡靜態(tài)庫,,因為生成可執(zhí)行文件之后就不用再管了(盡管對空間占用有點不負(fù)責(zé)任),。 Cython 和靜態(tài)庫結(jié)合 然后回到我們的主題,我們的重點是 Cython 和它們的結(jié)合,,當(dāng)然先對靜態(tài)庫和動態(tài)庫有一定的了解是必要的,。下面來看看 Cython 要如何引入靜態(tài)庫,這里我們編寫斐波那契數(shù)列,,然后生成靜態(tài)庫,。當(dāng)然為了追求刺激,這里采用 CGO 進(jìn)行編寫,。
關(guān)于 CGO 這里不做過多介紹,你也可以使用 C 來編寫,,效果是一樣的,。然后我們來使用 go build 根據(jù) go 源文件生成靜態(tài)庫: go build -buildmode=c-archive -o 靜態(tài)庫文件 go源文件
然后還需要一個頭文件,這里定義為 go_fib.h:
里面只需要放入一個函數(shù)聲明即可,,具體實現(xiàn)在 libfib.a 中,,然后編寫 Cython 源文件,文件名為 wrapper_gofib.pyx:
函數(shù)的具體實現(xiàn)邏輯是以源文件形式存在,、還是以靜態(tài)庫形式存在,,實際上并不關(guān)心。然后是編譯腳本 setup.py:
然后我們執(zhí)行 python3 setup.py build,,因為我現(xiàn)在使用的是 Linux,,所以需要輸入 python3,要是輸入 python 會指向 python2,。 執(zhí)行成功之后,,會生成一個 build 目錄,我們將里面的擴(kuò)展模塊移動到當(dāng)前目錄,,然后進(jìn)入交互式 Python 中導(dǎo)入它,,看看會有什么結(jié)果。 此時我們就將 Cython, Go, C, Python 給結(jié)合在一起了,,當(dāng)然你可以再加入 C 源文件,、或者 C 生成的庫文件,怎么樣,,是不是很好玩呢,。如果用 Go 寫了一個程序,那么就可以通過編譯成靜態(tài)庫的方式,,嵌入到 Cython 中,,然后再生成擴(kuò)展模塊交給 Python 調(diào)用。之前我本人也將 Python 和 Go 結(jié)合起來使用過,,只不過當(dāng)時是編譯成的動態(tài)庫,,然后通過 Python 的 ctypes 模塊調(diào)用的。 注意:無論是這里的靜態(tài)庫還是一會要說的動態(tài)庫,,我們舉的例子都會比較簡單,。但實際上我們使用 CGO 的話,內(nèi)部是可以編寫非常復(fù)雜的邏輯的,,因此我們需要注意 Go 和 C 之間內(nèi)存模型的差異,。因為 Python 和 Go 之間是無法直接結(jié)合的,但是它們都可以和 C 勾搭上,,所以需要 C 在這兩者之間搭一座橋,。 但是不同語言的內(nèi)存模型是不同的,因此當(dāng)跨語言操作同一塊內(nèi)存時需要格外小心,,比如 Go 的導(dǎo)出函數(shù)不能返回 Go 的指針等等,。所以里面的細(xì)節(jié)還是比較多的,當(dāng)然我們這里的主角是 Cython,,因此 Go 就不做過多介紹了,。
Cython 和動態(tài)庫結(jié)合 然后是 Cython 和 動態(tài)庫結(jié)合,,我們還用剛才的 go_fib.go,而 Go 生成動態(tài)庫的命令如下: go build -buildmode=c-shared -o 動態(tài)庫文件 go源文件
動態(tài)庫的話我們只需要生成 libfib.so 即可,,然后其它地方不做任何改動,,直接執(zhí)行 python3 setup.py build 生成擴(kuò)展模塊,因為加載動態(tài)庫和加載靜態(tài)庫的邏輯是一樣的,。而我們的動態(tài)庫和剛才的靜態(tài)庫的名字也保持一致,,所以整體不需要做任何改動。 整體效果和 C 使用動態(tài)庫的表現(xiàn)是一致的,,仍然優(yōu)先尋找動態(tài)庫,,并且還要將動態(tài)庫所在路徑加入到 ld.so.conf 中。如果在動態(tài)庫和靜態(tài)庫同時存在的情況下,,想使用靜態(tài)庫的話,,那么可以這么做:
當(dāng)然我們這里使用 Go 來生成庫文件實際上有點刻意了,因為主要是想展現(xiàn) Cython 的強(qiáng)大之處,。但其實使用 C 來生成庫文件也是一樣的,,因為我們使用 Go 本質(zhì)上也是將 Go 的代碼轉(zhuǎn)成 C 的代碼(因此叫 CGO),只不過用 Go 寫代碼肯定比用 C 寫代碼舒服,,畢竟 Go 是一門帶垃圾回收的高級語言,。 至于 Go 和 C 之間怎么轉(zhuǎn),那就不需要我們來操心了,,Go 編譯器會為我們處理好一切,。正如我們此刻學(xué)習(xí) Cython 一樣,用 Cython 寫擴(kuò)展肯定比用 C 寫擴(kuò)展舒服,,但 Cython 代碼同樣也是要轉(zhuǎn)成 C 的代碼,,至于怎么轉(zhuǎn),也不需要我們來操心,,Cython 編譯器會為我們處理好一切,。 以上就是 Cython 和庫文件(靜態(tài)庫、動態(tài)庫)之間的結(jié)合,,注:Cython 引入庫文件的相關(guān)操作都是基于 Linux,,至于 Windows 如何引入庫文件可以自己試一下。 小結(jié) 在該系列的最開始我們就說過,,其實可以將 Cython 當(dāng)成兩個身份來看待:如果是編譯成 C,,那么可以看作是 Cython 的 '陰',;如果作為膠水連接 C 或者 C++,那么可以看作是 Cython 的 '陽',。 但其實兩者之間并沒有嚴(yán)格區(qū)分,,一旦在 cdef extern from 塊中聲明了 C 函數(shù),就可以像 Cython 本身定義的常規(guī) cdef 函數(shù)一樣使用,。并且對外而言,,在使用 Python 調(diào)用時,沒有人知道里面的方法是我們自己辛辛苦苦編寫的,,還是調(diào)用了其它已經(jīng)存在的,。 這次我們介紹了 Cython 的一些接口特性和使用方法,感受一下它包裝 C 函數(shù)是多么的方便,。而 C 已經(jīng)存在很多年了,,擁有大量經(jīng)典的庫,通過 Cython 我們可以很輕松地調(diào)用它們,。 當(dāng)然不只是 C,,Cython 還可以調(diào)用同樣被廣泛使用的 C++ 中的庫函數(shù),但由于我本人不擅長 C++,,因此有興趣可以自己了解一下,。 E N D |
|