Golang 中如何用 CGO 與 C 之間做一個快取 buffer

Tosone發表於2019-02-22

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_inbuffer_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 這邊出現了問題一般都會有錯誤的堆疊的列印。這個小例子裡邊沒有考慮太多的記憶體釋放這方面的問題,實際專案中參考這段程式碼的時候千萬注意,坑了別找我。

相關文章