[C Extended] Object với Closure function

Hàm closure trong C là gì?

Ở đây, bạn có thể hiểu theo Javascript, nói khái quát thì nó là một hàm đóng kín, search Google để tìm hiểu thêm nhé 😃

Một điểm đặc biệt nữa là hàm closure còn có khả năng trữ một biến chứa giá trị bạn đã gán trước đó vào hàm, khi sử dụng (trong khai báo), bạn có thể truy cập đến biến đó.

      [closure] <- alloc(data)
          |
          V            // stack
call -> [ptr](param) --| data
                       | param

Fake object phong cách C thuần

Thông thường, muốn fake một object (theo C++), ta sẽ khai báo một struct như sau.

typedef struct foo {
    int val;
    void (*put)(struct foo *self); // ko gán giá trị vào ngay được
    // nên mình khai báo hàm rồi gán sau
} foo;

void foo_put(struct foo *self) {
    printf("value is %d\n", self->val);
}

Khởi tạo object & test thử.

foo *myfoo = calloc(1, sizeof(foo));
myfoo->put = foo_put; // gán foo_put vào put

myfoo->val = 123;
myfoo->put(myfoo); // << value is 123
// free(myfoo); // free

Right, không một vấn đề gì. Nhưng code ở đây khá là dài dòng, thay vào đó thì code bên C++ không sướng hơn sao. Một điều bất tiện nữa đó chính là thừa tham số đầu tiên - self.

void (*print)() = (void*)myfoo->put; 
print(); // wrong!
print(myfoo); // right!

Trong code trên, print cứ hiểu là đã bỏ qua self, nhưng thực tế thì ta cần self để điều khiển thằng myfoo->put, vì self là tham số đầu tiên của nó. Tất nhiên cũng không thể sử dụng phương pháp fake truyền thống này cho callback (print).

Sử dụng closure function

Như đã nói ở phần đầu thì hàm closure có thể lưu trữ một giá trị mà ta đã gán trước đó vào hàm, ta sẽ sử dụng giá trị đó làm self, và số lượng các tham số sẽ ứng với hàm thông thường, tương tự như C++.

C   self->fun  [addr] | [self, param_1, ..., param_N]
    self->clor [addr, self] | [param_1, ..., param_N]
C++ this->func [addr, self] | [param_1, ..., param_N]

Code trong tưởng tượng.

typedef struct foo2 {
    int val;
    void (*put)(); // giống C++ chưa?
                   // void foo2.put();
} foo2;

void foo2_put() {
    printf("value is %d", self->val); 
    // self đâu?, đang tưởng tượng mà
}

foo2 *bar = alloc(sizeof(foo2));
bar->put = think_about_closure(foo2_put, bar); // bar => self

bar->val = 69;
bar->put(); // << value is 69

Hãy nhìn xem, quá tuyệt phải không, nhưng đó chỉ là tưởng tượng. Vậy làm thế nào để có closure function?

Nếu bạn search Google hoặc Github về C closure function thì chắc cũng có khá nhiều, nhưng mình khuyên nên dùng libffcall hoặc clofn, hai thằng này dễ sử dụng & cho hiệu năng cao.

#include "clofn.h"

def_clofn(void, foo2_put, size_t, data, (void), {
    foo2 *self = (foo2*)data;
    printf("value is %d", self->val);
})

foo2 *bar = alloc(sizeof(foo2));
bar->put = new_clofn(foo2_put, bar); // bar => self

bar->val = 69;
bar->put(); // << value is 69

Để thực tế hóa vấn đề, bạn có thể tham khảo qua repo này của mình: wobj (thực ra mục đích PR trá hình thôi 😃)


Vài vấn đều có thể bạn sẽ thắc mắc:

Tại sao lại không sử dụng C++ cho lành, làm chi mô cho tốn sức?

Theo mình thì C++ khá là ràng buộc, không tự do như C tí nào cả, tương tự như Rust với C++ vậy.

Object khỉ gì mà chả có đa hình, trừu tượng, kế thừa...?

Không phải OOP nào cũng nhất thiết phải có ba cái thứ đó (như PHP, Lua...), chắc bạn đã bị ngấm post nói về những điều phải có của OOP đâu đó trên mạng rồi. Quay trở lại, theo mình thì đa hình, trừu tượng có rồi, kế thừa thì dùng memcpy() vẫn ok mà.

Sao test code trên Visual Studio mà không thấy gì xảy ra, bug ư?

Có thể là bug, đối với VS bắt buộc phải set Release mode trước khi build.