FFI === 似乎蠻多人對 FFI, ABI issue 沒有深入探討 筆者也早在埋坑清單上面留下 ABI issue 預計要編寫,然而遇到另外一篇專案原作者表示需要用到其他不同語言的三方依賴庫,而他認為這部份如果不使用跟三方相依的程式語言會有開發困難,於是跟著寫三方語言。 筆者認為這部份,需要考驗程式設計師對於程式如何運作的基本功。所以先撰寫這篇 FFI 吧! ## 如何在 Python 呼叫 C++ 撰寫的程式? 眾所周知,C++ 多數情況跑得比 Python 快,很多時候在 Python domain 做運算過度耗時,我們有沒有辦法把一些 sub-routine 搬到 C++ domain 執行呢? 顯然答案是肯定的 ### binding tools 諸如 [cppyy](https://cppyy.readthedocs.io/en/latest/), [pybind11](https://github.com/pybind/pybind11), [swig](https://www.swig.org/), [ctype](https://docs.python.org/3/library/ctypes.html) 等等,不論這些工具如何闡述他們 JIT 的部分,最終都是以 run time library 的形式被呼叫。我們可以做出以下實驗: 使用 strace 追蹤 system call ```c // file name: hello.c #include <stdio.h> void hello(void) { printf("Hello, world!\n"); } ``` Compile C to shared library: ```shell cc -fPIC -shared -o libhello.so ./hello.c ``` The Python code: ```python import ctypes if __name__ == '__main__': lib = ctypes.cdll.LoadLibrary('./libhello.so') lib.hello() print('Done!') ``` 執行效果: ![](https://hackmd.io/_uploads/SJQZzG0-T.png) ![](https://hackmd.io/_uploads/rkLb6GOMT.png) ~~因為 mac 要開 dtruss 很麻煩,所以寫到一半跑去開 Linux(但其實也隔了幾天)~~ 而後我們看一下 strace: ```shell strace python ./main.py 2>&1 | less -N ``` 前面其實有一些 python 在開始執行的 library loading 這部份不用看 > 除非你跟我一樣曾經自幹 [standalone python](https://github.com/25077667/standalone-python) 需要深入了解 How Python bootstraping ``` ... 753 newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=2518, ...}, AT_EMPTY_PATH) = 0 754 read(3, "import sys\nfrom ctypes import *\n"..., 2519) = 2518 755 read(3, "", 1) = 0 756 close(3) = 0 757 openat(AT_FDCWD, "./libhello.so", O_RDONLY|O_CLOEXEC) = 3 758 read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\0\0\0\0\0\0\0\0"..., 832) = 832 759 newfstatat(3, "", {st_mode=S_IFREG|0755, st_size=14984, ...}, AT_EMPTY_PATH) = 0 760 getcwd("/tmp/foo", 128) = 9 761 mmap(NULL, 16408, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fe3e9a3c000 762 mmap(0x7fe3e9a3d000, 4096, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1000) = 0x7fe3e9a3d000 763 mmap(0x7fe3e9a3e000, 4096, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x2000) = 0x7fe3e9a3e000 764 mmap(0x7fe3e9a3f000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x2000) = 0x7fe3e9a3f000 765 close(3) = 0 766 mprotect(0x7fe3e9a3f000, 4096, PROT_READ) = 0 767 newfstatat(1, "", {st_mode=S_IFIFO|0600, st_size=0, ...}, AT_EMPTY_PATH) = 0 768 write(1, "Done!\n", 6Done! 769 ) = 6 770 rt_sigaction(SIGINT, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=SA_RESTORER|SA_ONSTACK, sa_restorer=0x7fe3e925c710}, {sa_handler=0x7fe3e9579e10, sa_mask=[], sa_flags=SA_RESTORER|SA_ONSTACK, sa_restorer=0x7fe3e925c710}, 8) = 0 771 munmap(0x7fe3e9ab9000, 16384) = 0 772 write(1, "Hello, world!\n", 14Hello, world! 773 ) = 14 774 exit_group(0) = ? 775 +++ exited with 0 +++ ``` 在 line 754 真正把檔案抓進來 Python interpretation,這邊可以看到接下來解析過程不需要 call syscall 而後直接在 line 757 直接打了一個 system call 說:`openat(AT_FDCWD, "./libhello.so", O_RDONLY|O_CLOEXEC) = 3` 顯然就是 open system call 的新版, system call 257 [link](https://github.com/torvalds/linux/blob/master/arch/x86/entry/syscalls/syscall_64.tbl) - 把檔案讀進來 - 取當前目錄 - 把檔案映射上記憶體 - 取得起始位置 `0x7fe3e9a3c000` - 對不同記憶體區段(偏移量)映射不同權限 - 關檔案 - 設定記憶體屬性保護 - (跳上去執行,此步驟不需要系統呼叫,因為已經在 process 當中) - 在 `libhello.so` 當中呼叫到 `write(1, "Done!\n", 6Done!)` 如此我們可以知道 Process 就是這樣透過 dynamic linking,跳上去外部程式語言的呼叫。 ## C++ 如何對接 C 語言 姑且不論讀者使用 C++ 對 C 語言 compatible part ,進而直接呼叫 C library 在 libstdc++ 的實做。 請問 C++ 如何與 C 語言進行互動? ### extern "C" 可能讀者會在 third-party library 看到這樣的語句,並且使用 macro 包起來,確定 defined \_\_cplusplus 才採用這樣的宣告。 會什麼我們會需要這樣呢? 這邊筆者留另外一篇講解 C++ 的 name mangling,讀者可自行搜尋。 而這邊 extern "C" 見:https://en.cppreference.com/w/cpp/language/language_linkage 是指我在這邊「宣告」一個需要被 linker link 進來的 declaration 跟編譯器保證,這個東西在外部存在,請交給 linker 執行,至於是 compile time 的 linking,還是 run time 的 linking (loading) 都可以,人工保證有,請 compiler 先不要亂動。 所以我們可以做到 C++ 跨程式語言到 C 語言,進行執行。 見下面筆者在學生時期,懶的寫兩份 code 所作的例子: ```cpp // stack.hpp #ifndef __STACK_HPP__ #define __STACK_HPP__ struct stack; class Stack { struct stack *my_stack; public: Stack(); ~Stack(); void push(int x); int pop(); }; #endif ``` ```cpp // stack.cpp #include "stack.h" #include "../../src/c/stack.h" Stack::Stack() { this->my_stack = ::new_stack(); } Stack::~Stack() { ::delete_stack(this->my_stack); } void Stack::push(int x) { ::push(this->my_stack, x); } int Stack::pop() { return ::pop(this->my_stack); } ``` 這邊筆者就直接呼叫 outer namespace 的 `new_stack` 與 `delete_stack` function ```c // stack.h #ifndef __STACK_H__ #define __STACK_H__ #define STACK_SIZE 100 struct stack { int sp; int stk[STACK_SIZE]; }; #ifdef __cplusplus extern "C" { #endif extern void push(struct stack *_this, int x); extern int pop(struct stack *_this); extern struct stack *new_stack(); extern void delete_stack(struct stack *stk); #ifdef __cplusplus } #endif #endif ``` 這個 C 語言定義的 function 就是上面所呼叫到的地方。 上面 stack.cpp 這個 compilation unit 就不會直接看到 C 語言當中這幾個具體的定義: ```c extern void push(struct stack *_this, int x); extern int pop(struct stack *_this); extern struct stack *new_stack(); extern void delete_stack(struct stack *stk); ``` 而後才在另外一個 stack.c 才定義這幾個 function 該如何實做 ```c /* The lisked-list of single stack */ typedef struct ll_stack { struct stack *block; struct ll_stack *next; } ll_stack_t; typedef struct pool { ll_stack_t *me; struct pool *prev; } pool_t; //... void push(struct stack *this, int x) { /* The new comming element is out of range */ if (__glibc_unlikely(this->sp > 98)) { /* Get this block's ll_stack_t */ pool_t *curr = stack_pool; while (curr && curr->me && curr->me->block != this) curr = curr->prev; /* Create a new block */ /* Must be guaranteed which could be found! */ ll_stack_t *new_ll_stack = malloc(sizeof(ll_stack_t)); new_ll_stack->block = malloc(sizeof(struct stack)); new_ll_stack->next = curr->me->next; /* Copy old stack to new allocated */ memcpy(new_ll_stack->block, this, sizeof(struct stack)); curr->me->next = new_ll_stack; /* Initialize old stack */ this->sp = -1; } this->stk[++this->sp] = x; } //... ``` :::info 突擊測驗! 1. 請指出上述 push 函式還可以如何最佳化? hint: stack_pool 2. 請修改這個 push 可以 thread-safe,也請修改讓這個 push 能夠 reentrant 3. 請解釋 this pointer。 ::: ## Break ABI to Save C++ 這邊我們留到 ABI 篇再講