Giao Thức Buffers Cho Ngôn Ngữ C - nói dối e blog

Giao Thức Buffers Cho Ngôn Ngữ C

Dự án Protocol Buffers cho ngôn ngữ C

Tôi luôn cảm thấy chưa hài lòng với thiết kế mặc định của Google Protocol Buffers. Việc sinh ra hàng loạt mã C++ cho mỗi kiểu message đã khiến tôi cảm thấy khó chịu. Ngoài ra, phiên bản chính thức không có hỗ trợ cho C, và các thư viện C do bên thứ ba phát triển cũng không đáp ứng được mong đợi của tôi.

Thiết kế này khiến việc triển khai binding cho các ngôn ngữ động trở nên phức tạp. Hầu hết các ngôn ngữ động đều không có kiểm tra kiểu mạnh, nên việc sinh mã như vậy thật ra không mang lại nhiều lợi ích mà ngược lại gây hao phí hiệu năng nghiêm trọng (so với cách tiếp cận thông thường khi xây dựng thư viện binding). Chẳng hạn, thư viện Python chính thức hoàn toàn có thể sinh ra các hàm cần thiết tại runtime dựa trên mô tả giao thức, thay vì dùng công cụ ngoại tuyến để sinh mã.

Năm ngoái, tôi từng viết một thư viện phiên bản Lua. Để độc lập hoàn toàn với phiên bản chính thức, tôi thậm chí còn dùng lpeg để viết một parser cho file .proto. Chỉ với chưa đến 100 dòng mã Lua, tôi đã có thể phân tích được nội dung giao thức chứa trong file .proto, cho phép thư viện Lua trực tiếp tải các file mô tả giao thức dạng văn bản (chính công cụ này đã giúp tôi rất nhiều trong công việc hiện tại).

Gần đây, khi bắt đầu một dự án mới, tôi lại gặp vấn đề phân tích giao thức protobuf và quyết định giải quyết triệt để từ gốc rễ. Trong tháng trước, tôi dự định tận dụng luajit để xây dựng một thư viện thuần Lua. Tôi cho rằng sử dụng luajit và ffi có thể đạt được hiệu năng tốt. Tuy nhiên sau khi hoàn thành, tôi nhận thấy hiệu suất vẫn thua kém phiên bản C++ (chỉ đạt khoảng 25-33% tốc độ của C++), thậm chí còn kém hơn cả phương pháp kết hợp C và Lua mà tôi đã viết năm ngoái. Mặc dù vậy, phiên bản C kết hợp Lua năm ngoái lại có quá nhiều phụ thuộc giữa mã C và Lua. Vì vậy tôi quyết định viết lại hoàn toàn bằng C.

Khi đang làm dở, có người dùng mạng chỉ ra rằng một kỹ sư Google gần đây cũng đang làm dự án tương tự mang tên μpb. Dự án này được trình bày rất kỹ lưỡng trong một bài viết dài, về cơ bản cùng xuất phát từ mục tiêu tương tự của tôi. Tuy nhiên, thiết kế API của nó không được tốt, tôi thấy cách dùng khá phức tạp. Vì vậy điều này không làm ảnh hưởng đến quyết tâm hoàn thành dự án của riêng tôi.

Khó khăn trong việc thiết kế API cho phiên bản C chủ yếu đến từ việc C thiếu các cấu trúc dữ liệu cần thiết, không có cơ chế thu gom rác và thiếu thông tin siêu dữ liệu về kiểu dữ liệu.

Sau nhiều lần cân nhắc, tôi quyết định cung cấp hai bộ API để đáp ứng nhu cầu khác nhau.

Khi yêu cầu hiệu năng không quá cao, chỉ cần tiện lợi trong phát triển C, tôi thiết kế một bộ API đơn giản dễ dùng để thao tác với các message theo định dạng protobuf. Tôi gọi đây là message API.

Bộ API này gồm hai nhóm chính:

Đối với việc giải mã message, sử dụng các API liên quan đến rmessage:

1
2
3
4
5
6
7
struct pbc_rmessage * pbc_rmessage_new(struct pbc_env * env, const char * typename , struct pbc_slice * slice);
void pbc_rmessage_delete(struct pbc_rmessage *);
uint32_t pbc_rmessage_integer(struct pbc_rmessage * , const char *key , int index, uint32_t *hi);
double pbc_rmessage_real(struct pbc_rmessage * , const char *key , int index);
const char * pbc_rmessage_string(struct pbc_rmessage * , const char *key , int index, int *sz);
struct pbc_rmessage * pbc_rmessage_message(struct pbc_rmessage *, const char *key, int index);
int pbc_rmessage_size(struct pbc_rmessage *, const char *key);

Đối với việc mã hóa message, sử dụng các API liên quan đến wmessage:

1
2
3
4
5
6
7
struct pbc_wmessage * pbc_wmessage_new(struct pbc_env * env, const char *typename);
void pbc_wmessage_delete(struct pbc_wmessage *);
void pbc_wmessage_integer(struct pbc_wmessage *, const char *key, uint32_t low, uint32_t hi);
void pbc_wmessage_real(struct pbc_wmessage *, const char *key, double v);
void pbc_wmessage_string(struct pbc_wmessage *, const char *key, const char * v, int len);
struct pbc_wmessage * pbc_wmessage_message(struct pbc_wmessage *, const char *key);
void * pbc_wmessage_buffer(struct pbc_wmessage *, struct pbc_slice * slice);

Hàm pbc_rmessage_newpbc_rmessage_delete dùng để tạo và giải phóng cấu trúc pbc_rmessage. Các thông tin con như chuỗi con, message con được lấy ra từ cấu trúc này đều được quản lý vòng đời bởi đối tượng gốc. Điều này giúp người dùng không cần phải làm các thao tác quản lý bộ nhớ phức tạp.

Đối với các dữ liệu dạng repeated (lặp), tôi không thiết kế kiểu dữ liệu mới mà coi tất cả các trường bên trong message đều có khả năng lặp. Cách thiết kế này giúp đơn giản hóa đáng kể số lượng API cần thiết.

Chúng ta có thể dùng hàm pbc_rmessage_size để kiểm tra số lần lặp của một trường cụ thể. Nếu message không chứa trường đó, hàm sẽ trả về 0.

Tôi thống nhất tất cả các kiểu dữ liệu cơ bản thành ba dạng chính: integer, string, real. Kiểu bool được xử lý như integer. Kiểu enum có thể biểu diễn dưới dạng string hoặc integer. Dùng pbc_rmessage_string sẽ lấy được tên enum, còn pbc_rmessage_integer sẽ lấy được id tương ứng.

Hàm pbc_rmessage_message dùng để lấy một sub-message. Đối tượng trả về không cần phải hủy thủ công vì vòng đời của nó được gán vào đối tượng cha. Ngay cả khi message không chứa sub-message đó, API vẫn trả về một đối tượng hợp lệ với các giá trị mặc định.

Kiểu integer không phân biệt giữa số 32 bit và 64 bit. Khi bạn chắc chắn giá trị không vượt quá 32 bit thì tham số cuối cùng của pbc_rmessage_integer có thể truyền NULL để bỏ qua phần giá trị cao 32 bit.

Các hàm của wmessage hoạt động như việc liên tục đẩy dữ liệu vào một gói message chưa hoàn tất. Sau khi hoàn tất việc điền dữ liệu, bạn có thể dùng pbc_wmessage_buffer để lấy ra một slice chứa con trỏ và độ dài của vùng nhớ.

Lưu ý rằng khi dùng pbc_wmessage_integer để đẩy một số âm, bạn phải truyền -1 cho tham

0%