原生互通:用 C 寫 Teyru 的 native 方法
native 方法就是一個 C 函式:編譯器產生呼叫與宣告,你提供定義,兩者一起編譯成同一個執行檔。
Teyru 沒有 JNI、沒有 FFI 綁定產生器,也沒有執行期解譯層。native 方法就是一個
C 函式:編譯器產生呼叫、產生宣告,你提供定義,兩者一起編譯成同一個執行檔。
program.teyru ──┐
├─► C ─► clang/LLVM ─► 原生執行檔
impl.c ──────────┘1. 三步驟
第一步:宣告 native 方法。 寫在一般的類別裡,不需要 body:
class Native {
public static native int add(int a, int b)
public static native String greeting(String who)
public native int scale(int v)
private int factor
public Native(int factor) {
this.factor = factor
}
}第二步:讓編譯器產生宣告。
teyru build --native-header native.h program.teyru -o program程式裡有 native 方法又沒給 --native 時,這個命令寫完標頭檔就停下來,不會嘗試連結
(連結必定會以 undefined reference to 'tyn_...' 失敗),所以它會成功的結束、只留下
標頭檔。接著做第三步即可。
native.h 只包含你必須實作的東西:
#include "tyrt.h"
/* Native.add */
int32_t tyn_Native_add_I_I(int32_t a0, int32_t a1);
/* Native.greeting */
void * tyn_Native_greeting_String(void * a0);
/* Native.scale */
int32_t tyn_Native_scale_I(void *self, int32_t a0);第三步:實作,然後一起編譯。
#include "tyrt.h"
#include "native.h"
int32_t tyn_Native_add_I_I(int32_t a, int32_t b) { return a + b; }
void *tyn_Native_greeting_String(void *who) {
tystr *prefix = ty_str_new("hello, ", 7);
return ty_str_concat(prefix, (tystr *)who);
}
int32_t tyn_Native_scale_I(void *self, int32_t v) {
struct { tyobj obj; int32_t f_factor; } *me = self;
return v * me->f_factor;
}teyru build --native impl.c --native-header native.h program.teyru -o programtests/native/ 有一份完整可跑的版本,go test -run TestNative 會編譯並執行它。
2. 符號命名
tyn_<類別>_<方法>_<參數型別…>- 類別與方法名以
util.Mangle轉寫:.與$變成_。 - 參數型別是描述子:
Iint、Jlong、Ddouble、Ffloat、Zboolean、Bbyte、Sshort、Cchar、O型別變數,其他類別用其簡單名稱 (例如String)。 - 陣列是
A再加元素描述子:int[]是AI、int[][]是AAI、String[]是AString。 - 沒有參數就沒有尾綴。多載會自然得到不同的名字,不需要額外規則。
不必自己推導:--native-header 產生的就是這個名字。
3. 型別對應
| Teyru | C | 備註 |
|---|---|---|
int | int32_t | |
long | int64_t | |
short / byte | int16_t / int8_t | |
char | uint16_t | UTF-16 碼元 |
boolean | int32_t | 0 或 1 |
float / double | float / double | |
void | void | |
| 物件、陣列、介面、泛型 | void * | 執行期表示法,見下節 |
| 實例方法的接收者 | void *self(第一個參數) | 靜態方法沒有 |
物件參數一律是 void *,所以 native.h 不依賴產生出來的 struct 名稱,
可以在任何地方 include。
4. 讀寫 Teyru 物件
物件就是 tyobj 開頭的 C struct,欄位依宣告順序平鋪(父類別的欄位在前)。
要讀欄位就在 C 端宣告同樣的開頭:
struct { tyobj obj; int32_t f_factor; } *me = self;
int32_t f = me->f_factor;f_ 前綴是編譯器對欄位的命名,f_factor 就是 private int factor。
如果不想依賴版面,把值用一般的 Teyru getter 傳進來就好——原生的介面
越小,越不容易隨編譯器改動而失效。
執行期提供給原生程式碼的常用工具(都在 tyrt.h):
| 函式 | 用途 |
|---|---|
ty_str_new(const char *bytes, int64_t len) | 建立字串 |
ty_str_concat(tystr *a, tystr *b) | 串接 |
ty_str_len(tystr *s) | UTF-16 code unit 數(ulen),也就是 String.length() 回的那個數;要在原生程式碼裡走位元組時用的是 ty_str_blen(s)(blen)。儲存是 WTF-8、位元組接在標頭後面(TY_STR_DATA(s),沒有第二個指標),見〈語言參考〉§12 第 12 條。沒有夥伴的代理是三個位元組的形狀,而 getBytes/println 會把它寫成一個 ? |
ty_array_new(int64_t len, int64_t elemsize) | 建立陣列 |
ty_array_len(tyarr *a) | 陣列長度 |
ty_alloc(size_t) | 從 GC 堆積配置(會自動被回收) |
ty_throw(void *e) | 丟出 Teyru 例外 |
ty_itab(void *obj, int selector) | 取得介面方法的函式指標 |
5. 回呼:從 C 呼叫 Teyru
--native-header 也會輸出介面方法的 selector(數值由編譯器配置,這裡列出的是目前的
樣子)。名字是 TY_SEL_<介面>_<方法>_<參數描述子>,方法沒有參數時就沒有尾綴
(例如 TY_SEL_AUTOCLOSEABLE_CLOSE):
#define TY_SEL_TRANSFORM_TRANSFORM_I 275
int32_t tyn_Native_apply_Transform_I(void *t, int32_t v) {
int32_t (*fn)(void *, int32_t) =
(int32_t (*)(void *, int32_t))ty_itab(t, TY_SEL_TRANSFORM_TRANSFORM_I);
return fn(t, v);
}於是 C 可以呼叫任何實作該介面的 Teyru 物件——包括 lambda:
Transform t = x -> x * 10
System.out.println(Native.apply(t, 4)) // 406. 其他建置選項
| 旗標 | 用途 |
|---|---|
--native <file.c> | 加入一個 C 檔一起編譯(可重複) |
--link <arg> | 傳給連結步驟的參數,例如 --link -lm 或 --link libfoo.a |
--native-header <path> | 產生 native 方法的宣告 |
--cc <name> | 換 C 編譯器(預設 clang) |
--no-lto | 關閉 LTO(工具鏈不支援時會自動退回) |
7. 執行期唯一需要原生程式庫的一層:TLS
執行期是自足的——它只依賴 C 函式庫——除了 TLS。那一層寫在 OpenSSL 上,所以它是
一個自己的檔案(internal/runtime/src/tyrt_tls.c):只有程式的可達程式碼碰得到
它的其中一個函式時,建置才編譯它、才加上 -lssl -lcrypto。不用 TLS 的程式因此一個
位元組都不付——這是「執行期不需要附帶的函式庫」這個原則的例外,而它被關在一個檔案裡。
用 native 方法的程式與這件事無關:那是你自己的 C 檔與 --native,要連結什麼由你
決定。
這一層的要求是 OpenSSL 1.1,而它是一個前置處理器的 #error,不是一頁未宣告的
識別字:
#if OPENSSL_VERSION_NUMBER < 0x10100000L
#error "the TLS layer needs OpenSSL 1.1 or newer (SSL_set1_host, BIO_meth_new, TLS_client_method and SSL_CTX_set_min_proto_version are not in 1.0.x)"
#endifSSL_set1_host、BIO_meth_new、TLS_client_method 與
SSL_CTX_set_min_proto_version 都不在 1.0.x 裡,所以舊的 OpenSSL 在編譯那個檔案時
用一句話講清楚,而不是讓使用者在一個他沒寫過的執行期檔案裡逐個識別字地讀「未宣告的
識別字」。
沒有 OpenSSL 的目標是具名拒絕,不是連結階段失敗。 只有 POSIX 有這一層:
windows/amd64 的 mingw-w64 沒有 OpenSSL,darwin/amd64 與 darwin/arm64 的 macOS
出的是 SecureTransport——兩者都在寫出任何輸出檔之前被拒絕,訊息指名目標、原因與
可以改用的目標,而不是留給連結器去說 undefined reference to SSL_CTX_new(那會指名
一個程式作者從沒提過的函式庫裡的符號)。被拒絕的是程式:只要可達程式碼碰得到這一
層就編不出來,而可達性是產生出來的 C 算的,所以會用到反射的程式(帶著指名每個類別的
表格)即使從不呼叫 TLS 也算碰得到。
以 C 實作 native 方法的人要知道的就是這些:這一層不是你可以 include 的標頭,它是一個 執行期檔案,而它進不進執行檔由編譯器依可達性決定。API、政策與測試見 docs/language.md 的〈TLS〉。
8. 已知限制
- 沒有自動繫結。 標頭檔由編譯器產生,實作要自己寫;沒有 C++ 名稱修飾解析、 沒有結構描述子、沒有記憶體佈局談判。
native方法不能有 body。 建構子可以是 native(lib/02_string.teyru的String(String original)就是),--native-header會一併宣告它:符號是tyn_<類別>__init__<參數描述子>,實例建構子的第一個參數是void *self。- GC 不會搬移物件,所以 C 端可以放心保存
void *——但只在該物件還活著的時候。 若要在 C 端長期持有參照,請用ty_gc_register_static註冊一個根, 否則回收器會在下次回收時把物件收走。 native.h會隨編譯器版本變動。 符號命名穩定,但欄位f_版面是產生出來的; 把它當 ABI 用就要接受這件事。