cgo調(diào)用c動態(tài)庫實戰(zhàn)
這篇從一個需求開始說起。這個需求也很簡單,就是需要為某個硬件加密算法封裝一個接口,接口的邏輯是傳入唯一標(biāo)識UID,能生成加密后的加密串。
這篇從一個需求開始說起。這個需求也很簡單,就是需要為某個硬件加密算法封裝一個接口,接口的邏輯是傳入唯一標(biāo)識UID,能生成加密后的加密串。
但是麻煩的是,這個加密算法是另外一個部門使用C開發(fā)的。而Web是Golang開發(fā)的。所以這個情況,自然就想到了用上cgo。
“硬件部門提供一個c編譯的動態(tài)庫,web服務(wù)方通過cgo來加載動態(tài)庫,調(diào)用動態(tài)庫中的接口,生成加密串?!?/p>
這樣通過cgo連接的好處:
1 web開發(fā)方無需了解任何加密算法邏輯,保證了加密算法的安全。
2 各司其職,硬件部門負(fù)責(zé)算法開發(fā),web部門負(fù)責(zé)封裝實現(xiàn)。
那怎么使用cgo來加載動態(tài)庫呢?這里記錄一下。
假設(shè)硬件部門提供的接口如下:
int Producer(int in_encrypt_method,
const unsigned char* in_uid,
unsigned int in_uid_len,
unsigned char** out_chip_key,
unsigned int* out_chip_key_len);
還有一個動態(tài)庫so文件 ChipSdk.so,和頭文件 chip_key_producer.h。
而要用上這個動態(tài)庫的接口,我們弄明白兩個事情就可以了:如何加載動態(tài)庫, 和如何轉(zhuǎn)換數(shù)據(jù)結(jié)構(gòu)。
加載動態(tài)庫
cgo是go和c的橋梁,它的功能其實很強(qiáng)大,比如,在c中調(diào)用go寫的函數(shù),在go中調(diào)用c寫的函數(shù)。我們這里就只是用了一種:在go中調(diào)用c寫的動態(tài)庫。
據(jù)我了解,go中調(diào)用c寫的動態(tài)庫至少有兩種辦法。
第一種,將動態(tài)庫放在lib目錄中,使用C編譯器選項讓go程序能找到這個lib庫和頭文件。
其中C編譯器選項CFLAGS和LDFLAGS兩個選項。其中CFLAGS標(biāo)識頭文件的路徑。即
// #cgo CFLAGS: -I/home/jianfengye/chipkey/
// #include "chip_key_producer.h"
表示去/home/jianfengye/chipkey/這個目錄加載"chip_key_producer.h"的頭文件。
而LDFLAGS標(biāo)識去哪里讀取動態(tài)庫,讀取哪個動態(tài)庫,比如
// #cgo LDFLAGS: -L/home/jianfengye/chipkey -lChipSdk
表示讀取 /home/jianfengye/chipkey/ChipSdk.so
// #cgo CFLAGS: -I/home/jianfengye/chipkey/
// #cgo LDFLAGS: -L/home/jianfengye/chipkey -lChipSdk
// #include "chip_key_producer.h"
import "C"
import (
"encoding/base64"
"errors"
"fmt"
...
)
// EncryptChipKey 獲取密鑰
func EncryptChipKey(uid string, typ int) ([]byte, error) {
...
result, err := C.Producer(C.int(typ), ucharPtr, ulenPtr, outKeyPtr, outKeyLenPtr)
if result != 0 || err != nil{
return nil, errors.New(fmt.Sprintf("ProducerCall return %v, err: %v", result, err))
}
...
}
上面的代碼例子就表示了加載ChipSdk.so動態(tài)庫,header頭文件未chip_key_producer.h。使用的時候調(diào)用其中的Producer函數(shù)。
第二種,使用動態(tài)dlopen的方式來加載動態(tài)庫。
dlopen 能把一個動態(tài)庫加載到內(nèi)存中,返回一個handler,這個handler可以通過dlsym來加載動態(tài)庫中的函數(shù)符號。代碼如如
/*
#cgo LDFLAGS: -ldl
#include <dlfcn.h>
typedef int (*Producer)(int,
const unsigned char*,
unsigned int,
unsigned char**,
unsigned int*); // function pointer type
int ProducerCall(void* f, int in_encrypt_method,
const unsigned char* in_uid,
unsigned int in_uid_len,
unsigned char** out_chip_key,
unsigned int* out_chip_key_len) { // wrapper function
return ((Producer) f)(in_encrypt_method, in_uid, in_uid_len, out_chip_key, out_chip_key_len);
}
*/
import "C"
import (
"encoding/base64"
"encoding/hex"
"errors"
...
)
func EncryptKey(uid string, typ int) ([]byte, error) {
...
handle := C.dlopen(C.CString("/home/jianfengye/chipkey/ChipSdk.so"), C.RTLD_LAZY)
funcPtr := C.dlsym(handle, C.CString("Producer"))
...
result, err := C.ProducerCall(funcPtr, C.int(typ), ucharPtr, ulenPtr, outKeyPtr, outKeyLenPtr)
if result != 0 || err != nil {
return nil, errors.New(fmt.Sprintf("ProducerCall return %v, err: %v", result, err))
}
...
}
我們可以看到,實際上我們用注釋的方式自己寫了一個函數(shù)ProducerCall,它的第一個參數(shù)是void*f 表示的是函數(shù)符號,Producer,就是C.dlsym和C.dlopen獲取出來的函數(shù)符號。
在go代碼中,我們實際上是調(diào)用了C.ProducerCall這個函數(shù)。
類型轉(zhuǎn)換
加載動態(tài)庫的方式使用上面兩種方式任意一種都是可以的,但是類型轉(zhuǎn)換,就是統(tǒng)一的了。我們都要先把go中的某些類型轉(zhuǎn)換為c中的某些類型,同時要把函數(shù)調(diào)用返回的c中的某些類型再變化成go中的類型。
我們先分析下Producer這個接口的參數(shù),這是標(biāo)準(zhǔn)的c接口,其中的參數(shù)中輸入有3個,一個是encrypt_method標(biāo)識加密類型,另外兩個in_uid 和 in_uid_len 表示的實際是一個字符串,表示的是輸入的設(shè)備的唯一標(biāo)識。而輸出的兩個參數(shù),out_chip_key 和 out_chip_key_len 也是表示一個字符串,分別為這個字符串的首地址指針和長度指針。
這里主要參考柴大的 https://chai2010.cn/advanced-go-programming-book/ch2-cgo/ch2-03-cgo-types.html 類型轉(zhuǎn)換一篇。
輸入轉(zhuǎn)換
先看輸入?yún)?shù),在go中,我們有的是一個string,如何把string變成unsigned char*和unsigned int呢?
首先和unsigned char* 對應(yīng)的結(jié)構(gòu)是 uint8數(shù)組
所以我們先把string變成uint8數(shù)組。
uidUint8 := make([]uint8, 0, len(uid))
for i := 0; i < len(hexUid); i++ {
uidUint8 = append(uidUint8, uint8(uid[i]))
}
然后,我們從uint8的slice結(jié)構(gòu)中可以獲取到指針信息和lenth信息。
var arr0Hdr = (*reflect.SliceHeader)(unsafe.Pointer(&uidUint8))
ucharPtr := (*C.uchar)(unsafe.Pointer(arr0Hdr.Data))
ulenPtr := C.uint(uint(arr0Hdr.Len))
這段就有點繞,首先將uidUint8這個slice變化成reflect.SliceHeader結(jié)構(gòu),然后從這個結(jié)構(gòu)中獲取到Data和Len字段。這兩個字段是slice結(jié)構(gòu)的內(nèi)部字段。
再接著,將Data結(jié)構(gòu)通過unsafe.Pointer,轉(zhuǎn)換為*C.uchar,也就是c中的uchar*。
同樣,把Len結(jié)構(gòu)轉(zhuǎn)換為C.uint,也就是c中的uint結(jié)構(gòu)。
這樣ucharPtr和ulenPtr就是C中可以直接使用的數(shù)據(jù)結(jié)構(gòu)了。
輸出轉(zhuǎn)換
接著看輸出轉(zhuǎn)換,我們從c函數(shù)中獲取到的是unsigned char** out_chip_key 和 unsigned int* out_chip_key_len
實際上我們想要的是返回[]byte,那么我們怎么做呢?
同樣我們也想到,slice數(shù)組都是(*reflect.SliceHeader)結(jié)構(gòu),那么我們創(chuàng)建一個[]byte,轉(zhuǎn)化為SliceHeader結(jié)構(gòu),然后將這個sliceHeader結(jié)構(gòu)的Data和Len的指針傳遞到c函數(shù)中,c函數(shù)就會修改這兩個指針指向的地址,從而達(dá)到將[]byte賦值的效果。
outKey := make([]uint8, 0, 0)
var outKeyHdr = (*reflect.SliceHeader)(unsafe.Pointer(&outKey))
outKeyPtr := (**C.uchar)(unsafe.Pointer(outKeyHdr.Data))
outKeyLenPtr := (*C.uint)(unsafe.Pointer(&outKeyHdr.Len))
result, err := C.ProducerCall(funcPtr, C.int(typ), ucharPtr, ulenPtr, outKeyPtr, outKeyLenPtr)
if result != 0 || err != nil {
return nil, errors.New(fmt.Sprintf("ProducerCall return %v, err: %v", result, err))
}
outKeyLen := uint32(*outKeyLenPtr)
outKeyHdr.Data = uintptr(unsafe.Pointer(*outKeyPtr))
outKeyHdr.Len = int(outKeyLen)
outKeyHdr.Cap = int(outKeyLen)
var ret []byte
for i := 0; i < int(outKeyLen); i++ {
ret = append(ret, byte(outKey[i]))
}
這里我們創(chuàng)建了一個[]uint8的數(shù)組,然后將對應(yīng)的Data,Len的指針通過unsafe.Pointer的方式轉(zhuǎn)換成對應(yīng)的C的指針結(jié)構(gòu)。
然后傳遞進(jìn)入C的函數(shù),當(dāng)c函數(shù)對指針賦值后,我們再將 (**C.uchar) 指向的 *uchar的地址轉(zhuǎn)換為uintptr賦值給Data。
同時把(*C.uint)指向的C.uint賦值給Len和Cap。
這樣C函數(shù)返回的uchar* 就指向給了[]uint8了。
最后我們再把[]uint8變成[]byte就行了。
總而言之,不管是輸出轉(zhuǎn)換還是輸入轉(zhuǎn)換,這里就是兩個原則:
1 基本類型通過 C.xxx來進(jìn)行互相轉(zhuǎn)換
2 指針類型通過unsafe.Pointer進(jìn)行互相轉(zhuǎn)換
總結(jié)
在生產(chǎn)過程中,還要記得cgo也有可能panic,最好在調(diào)用cgo的前后記得recover。
基本上弄懂了cgo的加載機(jī)制和類型轉(zhuǎn)換兩個邏輯和原理,cgo就算入門了。網(wǎng)上對cgo的使用介紹文章確實不多,不過go編譯器集成cgo已經(jīng)集成很不錯了,一旦你的類型轉(zhuǎn)換不符合要求,編譯的時候都會有很明確的錯誤指示了。
cgo就像是粘合劑,將golang和c連接起來了。


Hi,我是軒脈刃,一個名不見經(jīng)傳碼農(nóng),體制內(nèi)的小憤青,躁動的騷年,2022年想堅持寫一些學(xué)習(xí)/工作/思考筆記,謂之倒逼學(xué)習(xí)。歡迎關(guān)注個人公眾號:軒脈刃的刀光劍影。
推薦閱讀
