CGO không hỗ trợ trực tiếp tính năng về class trong C++. Nguyên nhân đến từ việc CGO không có hỗ trợ cho cú pháp C++, ngoài ra C++ cũng không có sẵn một Application Binary Interface (ABI) nào cả.
Nhưng do C++ tương thích với C, do đó chúng ta có thể thêm một tập các hàm C cho interface như là cầu nối giữa C++ class và CGO. Dĩ nhiên, bởi vì CGO chỉ hỗ trợ kiểu dữ liệu của ngôn ngữ C, chúng ta không thể trực tiếp dùng C++ reference parameters và các tính năng khác.
Việc hiện thực packaging C++ class thành Object trong Go yêu cầu một số bước:
- Đầu tiên, C++ class được bọc bởi một interface C thuần,
- Tiếp theo hàm trong interface sẽ map với hàm của Go bằng CGO
- Cuối cùng là tạo ra đối tượng Go wrapper. Lúc này ta có thể hiện thực class C++ thành các phương thức sử dụng Go objects.
Để minh họa, chúng ta sẽ dựa trên str::string
để làm một class đơn giản MyBuffer
:
// my_buffer.h
#include <string>
struct MyBuffer {
std::string* s_;
// thêm vào constructor
MyBuffer(int size) {
this->s_ = new std::string(size, char('\0'));
}
// và destructor
~MyBuffer() {
delete this->s_;
}
// trả về kích thước buffer
int Size() const {
return this->s_->size();
}
// trả về con trỏ tới data
char* Data() {
return (char*)this->s_->data();
}
};
Tiếp theo là cách chúng ta sử dụng:
int main() {
auto pBuf = new MyBuffer(1024);
auto data = pBuf->Data();
auto size = pBuf->Size();
delete pBuf;
}
Chúng ta có thể ánh xạ từ khóa new
và delete
C++ sang C và tương tự các phương thức của đối tượng tới hàm của ngôn ngữ C.
Trong C chúng ta mong đợi class MyBuffer
được dùng như sau:
int main() {
MyBuffer* pBuf = NewMyBuffer(1024);
char* data = MyBuffer_Data(pBuf);
auto size = MyBuffer_Size(pBuf);
DeleteMyBuffer(pBuf);
}
Đặc tả file header my_buffer_capi.h
:
// my_buffer_capi.h
typedef struct MyBuffer_T MyBuffer_T;
MyBuffer_T* NewMyBuffer(int size);
void DeleteMyBuffer(MyBuffer_T* p);
char* MyBuffer_Data(MyBuffer_T* p);
int MyBuffer_Size(MyBuffer_T* p);
Sau đó chúng ta có thể định nghĩa các hàm wrapper dựa trên class C++ là MyBuffer
. File my_buffer_capi.cc
tương ứng như sau:
// my_buffer_capi.cc
#include "./my_buffer.h"
// chỉ ra các file C++ được include
// mục đích để hàm C gọi được C++
extern "C" {
#include "./my_buffer_capi.h"
}
// một class kế thừa từ `MyBuffer`, thực ra
// là hiện thực việc wrapper code
struct MyBuffer_T: MyBuffer {
MyBuffer_T(int size): MyBuffer(size) {}
~MyBuffer_T() {}
};
MyBuffer_T* NewMyBuffer(int size) {
auto p = new MyBuffer_T(size);
return p;
}
void DeleteMyBuffer(MyBuffer_T* p) {
delete p;
}
char* MyBuffer_Data(MyBuffer_T* p) {
return p->Data();
}
int MyBuffer_Size(MyBuffer_T* p) {
return p->Size();
}
Lúc này trở đi, MyBuffer_T
giao tiếp với CGO có thể thông qua việc truyền pointer.
Sau khi wrapping C++ class thành một C interface thuần, bước tiếp theo là chuyển đổi hàm C sang hàm Go.
Quá trình bọc hàm C thành một hàm Go là tương đối đơn giản. Chú ý rằng bởi vì package của chúng ta chứa cú pháp C++11, chúng ta cần flag #cgo CXXFLAGS: -std=c++11
để mở tùy chọn C++11
.
// my_buffer_capi.go
package main
/*
#cgo CXXFLAGS: -std=c++11
#include "my_buffer_capi.h"
*/
import "C"
type cgo_MyBuffer_T C.MyBuffer_T
// Để phân biệt, chúng ta thêm vào một tiền tố `cgo_`
// cho mỗi hàm được đặt tên trong Go.
// `cgo_MyBuffer_T` là một kiểu `MyBuffer_T` trong C
func cgo_NewMyBuffer(size int) *cgo_MyBuffer_T {
p := C.NewMyBuffer(C.int(size))
return (*cgo_MyBuffer_T)(p)
}
func cgo_DeleteMyBuffer(p *cgo_MyBuffer_T) {
C.DeleteMyBuffer((*C.MyBuffer_T)(p))
}
func cgo_MyBuffer_Data(p *cgo_MyBuffer_T) *C.char {
return C.MyBuffer_Data((*C.MyBuffer_T)(p))
}
func cgo_MyBuffer_Size(p *cgo_MyBuffer_T) C.int {
return C.MyBuffer_Size((*C.MyBuffer_T)(p))
}
Khi đóng gói một hàm C thành một hàm Go, bằng việc thêm vào kiểu cgo_MyBuffer_T
, chúng ta vẫn dùng kiểu của C bên dưới kiểu dữ liệu cho tham số đầu vào và cho giá trị trả về.
Sau khi bọc interface C thuần thành một hàm Go, chúng ta sẽ xây dựng một đối tượng Go wrapper.
Bởi vì cgo_MyBuffer_T
là một kiểu được import trong không gian ngôn ngữ C, không thể định nghĩa những phương thức cho riêng chúng, do đó chúng ta phải xây dựng một kiểu MyBuffer
sẽ .
// my_buffer.go
package main
import "unsafe"
// giữ buffer của ngôn ngữ C được trỏ tới bởi `cgo_MyBuffer_T`
type MyBuffer struct {
cptr *cgo_MyBuffer_T
}
func NewMyBuffer(size int) *MyBuffer {
return &MyBuffer{
cptr: cgo_NewMyBuffer(size),
}
}
func (p *MyBuffer) Delete() {
cgo_DeleteMyBuffer(p.cptr)
}
// vì kiểu slice của Go có chứa cả thông tin chiều dài,
// chúng ta có thể kết hợp hai hàm `cgo_MyBuffer_Data`
// và `cgo_MyBuffer_Size` vào trong phương thức `MyBuffer.Data`
func (p *MyBuffer) Data() []byte {
data := cgo_MyBuffer_Data(p.cptr)
size := cgo_MyBuffer_Size(p.cptr)
// trả về một slice ứng với buffer trong C
return ((*[1 << 31]byte)(unsafe.Pointer(data)))[0:int(size):int(size)]
}
Bây giờ chúng ta có thể dễ dàng sử dụng wrapped buffer object trong ngôn ngữ Go (ngầm bên trong là phần hiện thực std::string
C++)
package main
//#include <stdio.h>
import "C"
import "unsafe"
func main() {
// tạo ra 1024-byte buffer
buf := NewMyBuffer(1024)
defer buf.Delete()
// cấp phát string bằng copy
copy(buf.Data(), []byte("hello\x00"))
//trực tiếp lấy ra thông tin con trỏ của buffer
// và in nội dung của buffer bằng hàm `put` của C
C.puts((*C.char)(unsafe.Pointer(&(buf.Data()[0]))))
}
Để hiện thực việc đóng gói các đối tượng ngôn ngữ Go vào các class C++, cần có các bước như sau:
- Trước tiên ánh xạ đối tượng Go sang một id
- Sau đó export hàm interface C tương ứng dựa trên id.
- Cuối cùng đóng gói đối tượng C++ dựa trên hàm interface C.
Để cho dễ theo dõi, tôi đã xây dựng một đối tượng Person
trong Go, mỗi đối tượng có thông tin về tên và tuổi:
package main
type Person struct {
name string
age int
}
func NewPerson(name string, age int) *Person {
return &Person{
name: name,
age: age,
}
}
func (p *Person) Set(name string, age int) {
p.name = name
p.age = age
}
func (p *Person) Get() (name string, age int) {
return p.name, p.age
}
Nếu đối tượng Person muốn được truy cập trong C/C++, thì nó cần được truy cập thông qua interface C.
Tạo một file tương ứng với file đặc tả interface C:
// person_capi.h
#include <stdint.h>
typedef uintptr_t person_handle_t;
person_handle_t person_new(char* name, int age);
void person_delete(person_handle_t p);
void person_set(person_handle_t p, char* name, int age);
char* person_get_name(person_handle_t p, char* buf, int size);
int person_get_age(person_handle_t p);
Sau đó, các hàm C này được hiện thực bằng ngôn ngữ Go.
Cần lưu ý rằng khi export ra các hàm C thông qua CGO, cả kiểu của tham số đầu vào và kiểu của giá trị trả về đều không hỗ trợ sửa đổi hằng số const và cũng không hỗ trợ các hàm có tham số biến. Đồng thời như đã mô tả trong phần trước (chương 2.7), chúng ta không thể truy cập trực tiếp các đối tượng bộ nhớ Go trong C/C++ trong một thời gian dài. Vì vậy, chúng ta cần ánh xạ đối tượng Go thành một id số nguyên.
Sau đây là file person_capi.go
hiện thực các hàm trong interface C:
// person_capi.go
package main
//#include "./person_capi.h"
import "C"
import "unsafe"
//export person_new
func person_new(name *C.char, age C.int) C.person_handle_t {
// ánh xạ tới id thông qua `NewObjectId`
id := NewObjectId(NewPerson(C.GoString(name), int(age)))
// buộc id phải được trả về dưới dạng `person_handle_t`
return C.person_handle_t(id)
}
//export person_delete
func person_delete(h C.person_handle_t) {
ObjectId(h).Free()
}
//export person_set
func person_set(h C.person_handle_t, name *C.char, age C.int) {
p := ObjectId(h).Get().(*Person)
p.Set(C.GoString(name), int(age))
}
//export person_get_name
func person_get_name(h C.person_handle_t, buf *C.char, size C.int) *C.char {
p := ObjectId(h).Get().(*Person)
name, _ := p.Get()
n := int(size) - 1
bufSlice := ((*[1 << 31]byte)(unsafe.Pointer(buf)))[0:n:n]
n = copy(bufSlice, []byte(name))
bufSlice[n] = 0
return buf
}
//export person_get_age
func person_get_age(h C.person_handle_t) C.int {
p := ObjectId(h).Get().(*Person)
_, age := p.Get()
return C.int(age)
}
Các hàm interface khác dựa trên id được thể hiện bởi person_handle_t
, nhờ đó đối tượng Go tương ứng được parse theo id.
Một cách thực hiện phổ biến là:
extern "C" {
#include "./person_capi.h"
}
// tạo một class Person mới
struct Person {
// chứa một thành viên thuộc kiểu `person_handle_t`
// tương ứng với đối tượng Go
person_handle_t goobj_;
// tạo một đối tượng Go thông qua interface
// C trong hàm constructor của class Person
Person(const char* name, int age) {
this->goobj_ = person_new((char*)name, age);
}
// giải phóng đối tượng Go qua interface C trong hàm destructor
~Person() {
person_delete(this->goobj_);
}
void Set(char* name, int age) {
person_set(this->goobj_, name, age);
}
char* GetName(char* buf, int size) {
return person_get_name(this->goobj_ buf, size);
}
int GetAge() {
return person_get_age(this->goobj_);
}
}
Sau khi đóng gói, chúng ta có thể sử dụng nó như một class C++ bình thường:
#include "person.h"
#include <stdio.h>
int main() {
auto p = new Person("gopher", 10);
char buf[64];
char* name = p->GetName(buf, sizeof(buf)-1);
int age = p->GetAge();
printf("%s, %d years old.\n", name, age);
delete p;
return 0;
}
Trong lần hiện thực đóng gói các đối tượng C++ trước đây, mỗi lần tạo một instance Person mới, ta cần thực hiện hai lần cấp phát bộ nhớ: một lần cho phiên bản Person của C++ và một lần nữa cho phiên bản Person của ngôn ngữ Go.
Trong thực tế, phiên bản C++ của Person chỉ có một id thuộc kiểu person_handle_t
, được sử dụng để ánh xạ các đối tượng Go. Chúng ta có thể sử dụng person_handle_t
trực tiếp trong đối tượng C++.
Các phương pháp đóng gói được cải tiến như sau đây:
extern "C" {
#include "./person_capi.h"
}
struct Person {
// thêm một hàm thành viên static mới vào class Person
// để tạo một instance Person mới
static Person* New(const char* name, int age) {
// instance Person được tạo bằng cách gọi
// person_new, trả về kiểu `person_handle_tid`
// và chúng ta sử dụng nó làm con trỏ kiểu `Person*`
return (Person*)person_new((char*)name, age);
}
// trong các hàm thành viên khác, ta chuyển đổi con trỏ this
// thành một kiểu `person_handle_t` và sau đó gọi hàm
// tương ứng thông qua interface C.
void Delete() {
person_delete(person_handle_t(this));
}
void Set(char* name, int age) {
person_set(person_handle_t(this), name, age);
}
char* GetName(char* buf, int size) {
return person_get_name(person_handle_t(this), buf, size);
}
int GetAge() {
return person_get_age(person_handle_t(this));
}
};
Ở thời điểm này, ta đã đạt được mục tiêu export đối tượng Go dưới dạng interface C và sau đó đóng gói lại thành đối tượng C++ dựa trên interface C.
Các phương thức trong ngôn ngữ Go đều bị ràng buộc kiểu. Ví dụ nếu chúng ta xác định kiểu Int
mới dựa trên int, chúng ta có thể có phương thức riêng:
type Int int
func (p Int) Twice() int {
return int(p)*2
}
func main() {
var x = Int(42)
fmt.Println(int(x))
fmt.Println(x.Twice())
}
this
cho phép bạn tự do chuyển đổi các kiểu int và Int
để sử dụng các biến mà không thay đổi cấu trúc bộ nhớ cơ bản của dữ liệu gốc.
Để đạt được các tính năng tương tự trong C++, các cách hiện thực sau thường được sử dụng:
class Int {
int v_;
Int(v int) { this.v_ = v; }
int Twice() const{ return this.v_*2; }
};
int main() {
Int v(42);
printf("%d\n", v); // error
printf("%d\n", v.Twice());
}
Class Int
mới được thêm vào thêm phương thức Twice
nhưng mất quyền chuyển về kiểu int. Tại thời điểm này, không chỉ printf
không thể tự export giá trị của Int
mà còn mất tất cả các tính năng của operation kiểu int. this
là chủ ý của ngôn ngữ C++: đổi lợi ích từ việc sử dụng class lấy cái giá là mất tất cả các tính năng ban đầu của nó.
Nguyên nhân gốc rễ của vấn đề này là do kiểu con trỏ được cố định vào class trong C++. Hãy xem xét lại bản chất của this
trong ngôn ngữ Go:
func (this Int) Twice() int
func Int_Twice(this Int) int
Trong Go, kiểu của tham số receiver có chức năng tương tự như this
chỉ là một tham số hàm bình thường. Chúng ta có thể tự do chọn giá trị hoặc kiểu con trỏ.
Nếu nghĩ theo thuật ngữ C thì this
chỉ là một con trỏ void*
tới một kiểu bình thường và chúng ta có thể tự do chuyển đổi nó thành các kiểu khác.
struct Int {
int Twice() {
const int* p = (int*)(this);
return (*p) * 2;
}
};
int main() {
int x = 42;
printf("%d\n", x);
printf("%d\n", ((Int*)(&x))->Twice());
return 0;
}
Bằng cách này, chúng ta có thể xây dựng một đối tượng Int
bằng cách buộc con trỏ kiểu int thành con trỏ kiểu Int
thay vì hàm tạo mặc định (default constructor). Bên trong hàm Twice
, bằng cách chuyển con trỏ this
trở lại con trỏ int trong thao tác ngược lại, giá trị kiểu int ban đầu đã có thể được parse. Tại thời điểm này, kiểu Int
chỉ là một lớp vỏ trong thời gian biên dịch và không chiếm thêm bộ nhớ khi chạy.
Do đó, phương thức C++ cũng có thể được sử dụng cho các kiểu không phải class. C++ cho các hàm thành phần thông thường cũng có thể được liên kết với các kiểu. Chỉ có các phương thức ảo thuần túy được ràng buộc với đối tượng và đó là interface.
- Phần tiếp theo: Thư viện tĩnh và động
- Phần trước: Mô hình bộ nhớ CGO
- Mục lục