调用 C 库
cpro 可以直接调用 C 函数,分三步:声明、在 bu_anquan 里调用、复杂类型用指针传。
cpro 不自带任何 C 库,用到的库必须已经安装在机器上。
声明 C 函数
用 wailian(外联 / extern)照着 C 头文件写出函数签名,没有函数体:
wailian hs abs(x z32) z32
wailian hs pow(x f64, y f64) f64外联 函数 abs(x i32) i32
外联 函数 pow(x f64, y f64) f64extern fn abs(x i32) i32
extern fn pow(x f64, y f64) f64- 返回类型必须写,没有返回值写
kongbai - 不能有默认参数,不能标
kecuo - 编译器不读 C 头文件,也不检查签名是否和 C 一致。写错会导致程序行为错乱,务必照头文件核对。
调用
调用 C 函数必须在 bu_anquan 块里:
wailian hs pow(x f64, y f64) f64
rukou() {
bl r leixing f64 = 0.0
bu_anquan {
r = pow(2.0, 10.0)
}
dayin(r) // 1024
}外联 函数 pow(x f64, y f64) f64
入口() {
变量 r 类型 f64 = 0.0
不安全 {
r = pow(2.0, 10.0)
}
打印(r) // 1024
}extern fn pow(x f64, y f64) f64
rukou() {
var r type f64 = 0.0
unsafe {
r = pow(2.0, 10.0)
}
print(r) // 1024
}链接 C 库
libc 默认可用。其他库用 wailian ku(外联 库 / extern lib)声明,编译时自动链接:
wailian ku "m" // 链接 libm
wailian hs sin(x f64) f64外联 库 "m" // 链接 libm
外联 函数 sin(x f64) f64extern lib "m" // 链接 libm
extern fn sin(x f64) f64库要先装好开发包:
# Ubuntu / Debian
sudo apt install libsqlite3-dev
# Windows(在 MSYS2 UCRT64 窗口里)
pacman -S mingw-w64-ucrt-x86_64-sqlite3Windows 上必须装 UCRT 版的库(包名带 ucrt)。
能传哪些类型
| cpro | C |
|---|---|
z8 ~ z64、w8 ~ w64(i8 ~ i64、u8 ~ u64) | 定宽整数 |
f32 / f64 | float / double |
zhenjia(bool) | _Bool |
daizhi(T)(addr(T)) | T* |
hs(...) R(fn(...) R) | 函数指针,见 回调 |
kongbai(void) | 只用于返回值 |
字符串、数组、结构体本身、optional、借用、闭包都不能直接传,要换成指针,方法见下文。
字符串
| 拼音 | 中文 | 英文 | 方向 | 用途 |
|---|---|---|---|---|
c_chuan(s) | C串(s) | c_str(s) | cpro → C | C 用完就丢(如文件路径) |
c_chuan_kaobei(s) | C串拷贝(s) | c_str_copy(s) | cpro → C | C 要保存这个字符串,之后由 C 释放 |
c_zhuan_chuan(p) | C转串(p) | string_from_c(p) | C → cpro | 把 C 返回的字符串复制成 cpro 字符串 |
wailian hs puts(s daizhi(w8)) z32
wailian hs getenv(name daizhi(w8)) daizhi(w8)
rukou() {
bu_anquan {
puts(c_chuan("hello"))
bl home leixing zifuchuan = c_zhuan_chuan(getenv(c_chuan("HOME")))
dayin(home)
}
}外联 函数 puts(s 带址(u8)) i32
外联 函数 getenv(name 带址(u8)) 带址(u8)
入口() {
不安全 {
puts(C串("hello"))
变量 home 类型 string = C转串(getenv(C串("HOME")))
打印(home)
}
}extern fn puts(s addr(u8)) i32
extern fn getenv(name addr(u8)) addr(u8)
rukou() {
unsafe {
puts(c_str("hello"))
var home type string = string_from_c(getenv(c_str("HOME")))
print(home)
}
}缓冲区
C 给的一块内存要变成 cpro 数组,用 zhi_zhuan_shuzu(p, 个数, T)(址转数组 / array_from_addr),它会复制一份:
bu_anquan {
bl data leixing [w8] = zhi_zhuan_shuzu(buf, n, w8)
}不安全 {
变量 data 类型 [u8] = 址转数组(buf, n, u8)
}unsafe {
var data type [u8] = array_from_addr(buf, n, u8)
}T 只能是数字、zhenjia、zifu 这类简单类型。原来的缓冲区仍由你自己释放。
反过来把 cpro 数组交给 C,传首元素地址 quzhi(a[0]) 和长度 a.changdu()。取地址之后不要再往数组里追加元素,扩容会让地址失效。
结构体
结构体传指针。字段都是上表里的类型时,内存布局和 C 一致,可以直接传:
// C: struct Point { long x; long y; }; void draw(struct Point* p);
jiegouti Point {
x leixing z64,
y leixing z64,
}
wailian hs draw(p daizhi(Point)) kongbai
hs show(p &Point) kongbai {
bu_anquan {
bl c leixing Point = { x: p.x, y: p.y }
draw(quzhi(c))
}
}// C: struct Point { long x; long y; }; void draw(struct Point* p);
结构体 Point {
x 类型 i64,
y 类型 i64,
}
外联 函数 draw(p 带址(Point)) void
函数 show(p &Point) void {
不安全 {
变量 c 类型 Point = { x: p.x, y: p.y }
draw(取址(c))
}
}// C: struct Point { long x; long y; }; void draw(struct Point* p);
struct Point {
x type i64,
y type i64,
}
extern fn draw(p addr(Point)) void
fn show(p &Point) void {
unsafe {
var c type Point = { x: p.x, y: p.y }
draw(addr_of(c))
}
}字段里有字符串等 cpro 类型时,另定义一个只含 C 类型字段的"镜像"结构体,传之前转换一下:
// C: struct User { char* name; long id; }; void register_user(struct User* u);
jiegouti User { name leixing zifuchuan, id leixing z64 } // 业务用
jiegouti CUser { name leixing daizhi(w8), id leixing z64 } // 和 C 一一对应
wailian hs register_user(u daizhi(CUser)) kongbai
hs register(u &User) kongbai {
bu_anquan {
bl cu leixing CUser = { name: c_chuan(u.name), id: u.id }
register_user(quzhi(cu))
}
}// C: struct User { char* name; long id; }; void register_user(struct User* u);
结构体 User { name 类型 string, id 类型 i64 } // 业务用
结构体 CUser { name 类型 带址(u8), id 类型 i64 } // 和 C 一一对应
外联 函数 register_user(u 带址(CUser)) void
函数 register(u &User) void {
不安全 {
变量 cu 类型 CUser = { name: C串(u.name), id: u.id }
register_user(取址(cu))
}
}// C: struct User { char* name; long id; }; void register_user(struct User* u);
struct User { name type string, id type i64 } // 业务用
struct CUser { name type addr(u8), id type i64 } // 和 C 一一对应
extern fn register_user(u addr(CUser)) void
fn register(u &User) void {
unsafe {
var cu type CUser = { name: c_str(u.name), id: u.id }
register_user(addr_of(cu))
}
}二级指针出参
cpro 没有 daizhi(daizhi(T))。像 sqlite3_open(path, &db) 这种 C 往你的变量里写指针的接口,用一个 z64 变量接:
wailian ku "sqlite3"
wailian hs sqlite3_open(path daizhi(w8), pp daizhi(z64)) z32
hs open_db(path zifuchuan) kongbai {
bl slot leixing z64 = 0
bu_anquan {
sqlite3_open(c_chuan(path), quzhi(slot)) // C 把地址写进 slot
bl db leixing daizhi(w8) = luozhuanzhi(slot, w8) // 还原成句柄
}
}外联 库 "sqlite3"
外联 函数 sqlite3_open(path 带址(u8), pp 带址(i64)) i32
函数 open_db(path string) void {
变量 slot 类型 i64 = 0
不安全 {
sqlite3_open(C串(path), 取址(slot)) // C 把地址写进 slot
变量 db 类型 带址(u8) = 裸转址(slot, u8) // 还原成句柄
}
}extern lib "sqlite3"
extern fn sqlite3_open(path addr(u8), pp addr(i64)) i32
fn open_db(path string) void {
var slot type i64 = 0
unsafe {
sqlite3_open(c_str(path), addr_of(slot)) // C 把地址写进 slot
var db type addr(u8) = raw_to_addr(slot, u8) // 还原成句柄
}
}sqlite3* 这种不透明句柄在 cpro 里统一写成 daizhi(w8),只传递,不读里面的内容。C 的 NULL 写作 luozhuanzhi(0, T)。
回调
把 cpro 函数交给 C,由 C 回头调用(如 qsort 的比较函数)。直接传函数名即可,回调函数本身不需要 bu_anquan:
wailian hs qsort(base daizhi(z64), n w64, size w64,
cmp hs(daizhi(z64), daizhi(z64)) z32) kongbai
hs cmp_i64(a daizhi(z64), b daizhi(z64)) z32 {
ruguo (jiezhi(a) < jiezhi(b)) { fanhui 0 -[z32] 1 }
ruguo (jiezhi(a) > jiezhi(b)) { fanhui 1 }
fanhui 0
}
rukou() {
bl arr leixing [z64] = [5, 3, 8, 1]
bu_anquan { qsort(quzhi(arr[0]), 4, 8, cmp_i64) }
dayin(arr) // [1, 3, 5, 8]
}外联 函数 qsort(base 带址(i64), n u64, size u64,
cmp 函数(带址(i64), 带址(i64)) i32) void
函数 cmp_i64(a 带址(i64), b 带址(i64)) i32 {
如果 (解址(a) < 解址(b)) { 返回 0 -[i32] 1 }
如果 (解址(a) > 解址(b)) { 返回 1 }
返回 0
}
入口() {
变量 arr 类型 [i64] = [5, 3, 8, 1]
不安全 { qsort(取址(arr[0]), 4, 8, cmp_i64) }
打印(arr) // [1, 3, 5, 8]
}extern fn qsort(base addr(i64), n u64, size u64,
cmp fn(addr(i64), addr(i64)) i32) void
fn cmp_i64(a addr(i64), b addr(i64)) i32 {
if (deref(a) < deref(b)) { return 0 -[i32] 1 }
if (deref(a) > deref(b)) { return 1 }
return 0
}
rukou() {
var arr type [i64] = [5, 3, 8, 1]
unsafe { qsort(addr_of(arr[0]), 4, 8, cmp_i64) }
print(arr) // [1, 3, 5, 8]
}- 回调的参数和返回值同样只能用上表里的类型,编译器会逐个检查
- 闭包不能当回调:C 的回调位置只放得下一个函数地址,装不下闭包捕获的变量。回调要带状态,就用 C 接口里的
void* user_data参数,cpro 侧写成daizhi(T) - 回调函数不需要
daochu
C 包装文件
有些 C 接口没法直接对接,比如按值传结构体、类型宽度随平台变化(如 unsigned long)。这时在项目里写一个小 C 文件包装一下,用 wailian c 加进来:
/* shim.c,和 .cp 文件放在同一目录 */
#include <stdlib.h>
void div_wrap(int a, int b, int* quot, int* rem) {
div_t r = div(a, b);
*quot = r.quot;
*rem = r.rem;
}wailian c "shim.c"
wailian hs div_wrap(a z32, b z32, quot daizhi(z32), rem daizhi(z32)) kongbai外联 c "shim.c"
外联 函数 div_wrap(a i32, b i32, quot 带址(i32), rem 带址(i32)) voidextern c "shim.c"
extern fn div_wrap(a i32, b i32, quot addr(i32), rem addr(i32)) void- 支持
.c和.o文件,.c每次构建都会重新编译 - 路径相对写这条声明的
.cp文件,不能用绝对路径 - 适合几十到几百行的薄包装,不适合大量 C 代码
变参函数
printf 这类变参函数照 C 的样子声明,... 放在最后:
wailian hs printf(fmt daizhi(w8), ...) z32外联 函数 printf(fmt 带址(u8), ...) i32extern fn printf(fmt addr(u8), ...) i32变参位置的整数字面量按 z64 传,格式串要写 %lld。
常见报错
| 报错 | 原因 |
|---|---|
cannot find -lsqlite3 | 库的开发包没装 |
undefined reference to 'xxx' | 忘了写 wailian ku,或函数名拼错 |
| 运行时报找不到共享库 | 运行的机器上没装这个库 |
| 段错误、数据错乱 | 声明的签名和 C 不一致,照头文件逐个核对 |
| 必须放在 unsafe 块内 | C 调用没包 bu_anquan |
| 不能按值过 C 边界 | 直接传了结构体或字符串,改成指针或 c_chuan |
三条原则
- 签名自己负责:编译器相信你写的声明
- 内存各管各的:C 返回的指针 cpro 不管释放;
c_chuan借出的指针 C 不能保存 bu_anquan圈出边界:每个 C 调用都在bu_anquan里
