Golang 是一個不錯的語言,尤其是做一個快取中間層是非常非常容易的。比較常見的場景就是我們在讀一個很大很大的檔案的時候,我們是做不到一次載入檔案到記憶體的,Golang 可以做到一點一點的將檔案讀至末尾,慢慢處理完,相信很多語言也很容易做到這個,那如果在處理這個檔案的時候專案的主語言是 Golang 而需要用到一些用 C 寫好的模組那又該如何呢?如果讓一個程式設計師只用 C 來實現處理一個大檔案,那應該也是很容易的。Golang 對 C 的 binding 呢?
首先,我們先定義一個 C 的資料結構,也是一個很經典的資料結構:
typedef struct buffer_data { // 快取資料的結構體
uint8_t *ptr;
size_t size;
} buffer_data;
複製程式碼
既然是快取那就應該有一個明確的大小,至少會是一個固定的大小,更復雜的場景可能會根據具體的外部引數造成快取大小的變化。現在我們只是寫一個例子而已,簡單至上。然後就需要寫一個針對上邊的資料結構的初始化函式了。
// 初始化傳入的 buffer 的記憶體物件
buffer_data init_buffer() {
buffer_data buffer_in; // 傳入的資料物件
buffer_in.ptr = malloc(MAX_BUFFER_SIZE * sizeof(uint8_t));
buffer_in.size = 0;
return buffer_in;
}
複製程式碼
程式碼到此為止都很簡單,僅僅是申請一些空間給這個快取,並且快取大小固定。後邊的就稍微有一點點難度了。
快取的話,就需要兩個函式寫入讀出來操作。
讀入的操作來說就是將新的資料新增到快取的尾部,首先看一下程式碼:
// 從 Golang 中傳入資料到 c 的記憶體中,返回每次讀取的資料的數量
// 鑑於記憶體中不可以快取過多的資料,也是為了節省記憶體,那麼就需要每次僅將 buffer 填充的一定長度即可
// buffer 資料寫入的目的位置
// buf 寫入的資料
// buf_size 寫入資料的大小
// return 已經寫入資料的長度
int buffer_append(buffer_data *buffer, uint8_t *buf, int buf_size) {
if (buffer->size == MAX_BUFFER_SIZE) {
return 0;
}
pthread_mutex_lock(&buffer_in_mutex);
if (MAX_BUFFER_SIZE - buffer->size > buf_size) {
memcpy(buffer->ptr + buffer->size, buf, buf_size);
buffer->size += buf_size;
pthread_mutex_unlock(&buffer_in_mutex); // 解鎖執行緒
return buf_size;
}
memcpy(buffer->ptr + buffer->size, buf, MAX_BUFFER_SIZE - buffer->size);
int read = MAX_BUFFER_SIZE - buffer->size;
buffer->size = MAX_BUFFER_SIZE;
pthread_mutex_unlock(&buffer_in_mutex); // 解鎖執行緒
return read;
}
複製程式碼
由於寫入和讀出的操作是針對一個競態變數的互斥操作,那我們為了防止多執行緒操作的時候有問題,就需要針對 buffer 操作的時候加上一個執行緒鎖。程式碼的其他部分就比較容易理解了,僅僅是一些記憶體複製之類的。
最後就是那個讀出的操作了,讀出的操作稍稍有一點點複雜相比寫入,常規的做法就是將快取的頭部的資料取出一些,然後將後邊的未被讀到的資料往前移動,OK,看程式碼:
// 讀出資料
// buffer 資料來源
// buf 資料讀出之後儲存的位置
// buf_size 傳入的 buf 的申請的空間的大小
// return 讀出的資料的長度
int buffer_read(buffer_data *buffer, uint8_t *buf, int buf_size) {
if (buf_size == 0) {
return 0;
}
pthread_mutex_lock(&buffer_in_mutex);
if (buf_size >= buffer->size) {
int read = buffer->size;
memcpy(buf, buffer->ptr, buffer->size);
buffer->size = 0;
pthread_mutex_unlock(&buffer_in_mutex); // 解鎖執行緒
return read;
}
memcpy(buf, buffer->ptr, buf_size);
memmove(buffer->ptr,buffer->ptr+buf_size,buffer->size-buf_size);
buffer->size -= buf_size;
pthread_mutex_unlock(&buffer_in_mutex); // 解鎖執行緒
return buf_size;
}
複製程式碼
到此為止,我們的 C 的部分就完成了,其實這個還是有一點點簡陋,真正的應該是 buffer_in
和 buffer_out
一個輸入的快取,一個是輸出的快取,中間 C 對輸入的快取做一些處理,比如音訊格式轉換之類的,然後將資料給到 buffer_out
中,在 Golang 中接收資料做一些其他處理。
在這個例子中我們的 Golang 的作用僅僅是將資料一步步放到 buffer 中然後從 buffer 再讀出來。
package main
// #include <reader.h>
import "C"
import (
"io/ioutil"
"os"
"unsafe"
)
func main() {
var buffer = C.init_buffer()
bytes, _ := ioutil.ReadFile("reader.h.gch")
f, _ := os.Create("reader.out")
for len(bytes) != 0 {
var write = int(C.buffer_append(&buffer, (*C.uchar)(unsafe.Pointer(&bytes[0])), C.int(len(bytes))))
bytes = bytes[write:]
for {
var bytes = make([]byte, 1024)
var read = int(C.buffer_read(&buffer, (*C.uchar)(unsafe.Pointer(&bytes[0])), C.int(len(bytes))))
if read == 0 {
break
}
f.Write(bytes[:read])
}
}
f.Close()
}
複製程式碼
OK,程式碼到此為止就已經完了,大概的寫法就是這樣,裡邊沒什麼難點,只是有些同學開始做 CGO 的時候不太會寫二進位制的資料如何在 C 和 Golang 中傳遞而已。
完整的專案請檢視這裡: github.com/tosone/read…
另外要說明的一點是 Golang 和 C 在傳遞引數的時候是記憶體拷貝各自管理記憶體,CGO 底層有做中間記憶體的處理。所以如果你的程式關於 CGO 那一部分總是出現 Segmentation fault 那就是 C 的記憶體沒有管理好,仔細查查,也有可能是 CGO 底層出了問題,這個概率就比較小了,如果是 Golang 這邊出現了問題一般都會有錯誤的堆疊的列印。這個小例子裡邊沒有考慮太多的記憶體釋放這方面的問題,實際專案中參考這段程式碼的時候千萬注意,坑了別找我。