Skip to content

调用 C 库 ​

cpro 可以直接调用 C 函数,分三步:声明、在 bu_anquan 里调用、复杂类型用指针传。

cpro 不自带任何 C 库,用到的库必须已经安装在机器上。

声明 C 函数 ​

用 wailian(外联 / extern)照着 C 头文件写出函数签名,没有函数体:

cpro
wailian hs abs(x z32) z32
wailian hs pow(x f64, y f64) f64
cpro
外联 函数 abs(x i32) i32
外联 函数 pow(x f64, y f64) f64
cpro
extern fn abs(x i32) i32
extern fn pow(x f64, y f64) f64
  • 返回类型必须写,没有返回值写 kongbai
  • 不能有默认参数,不能标 kecuo
  • 编译器不读 C 头文件,也不检查签名是否和 C 一致。写错会导致程序行为错乱,务必照头文件核对。

调用 ​

调用 C 函数必须在 bu_anquan 块里:

cpro
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
}
cpro
外联 函数 pow(x f64, y f64) f64

入口() {
    变量 r 类型 f64 = 0.0
    不安全 {
        r = pow(2.0, 10.0)
    }
    打印(r)             // 1024
}
cpro
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)声明,编译时自动链接:

cpro
wailian ku "m"          // 链接 libm
wailian hs sin(x f64) f64
cpro
外联 库 "m"             // 链接 libm
外联 函数 sin(x f64) f64
cpro
extern lib "m"          // 链接 libm
extern fn sin(x f64) f64

库要先装好开发包:

bash
# Ubuntu / Debian
sudo apt install libsqlite3-dev

# Windows(在 MSYS2 UCRT64 窗口里)
pacman -S mingw-w64-ucrt-x86_64-sqlite3

Windows 上必须装 UCRT 版的库(包名带 ucrt)。

能传哪些类型 ​

cproC
z8 ~ z64、w8 ~ w64(i8 ~ i64、u8 ~ u64)定宽整数
f32 / f64float / 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 → CC 用完就丢(如文件路径)
c_chuan_kaobei(s)C串拷贝(s)c_str_copy(s)cpro → CC 要保存这个字符串,之后由 C 释放
c_zhuan_chuan(p)C转串(p)string_from_c(p)C → cpro把 C 返回的字符串复制成 cpro 字符串
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)
    }
}
cpro
外联 函数 puts(s 带址(u8)) i32
外联 函数 getenv(name 带址(u8)) 带址(u8)

入口() {
    不安全 {
        puts(C串("hello"))
        变量 home 类型 string = C转串(getenv(C串("HOME")))
        打印(home)
    }
}
cpro
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),它会复制一份:

cpro
bu_anquan {
    bl data leixing [w8] = zhi_zhuan_shuzu(buf, n, w8)
}
cpro
不安全 {
    变量 data 类型 [u8] = 址转数组(buf, n, u8)
}
cpro
unsafe {
    var data type [u8] = array_from_addr(buf, n, u8)
}

T 只能是数字、zhenjia、zifu 这类简单类型。原来的缓冲区仍由你自己释放。

反过来把 cpro 数组交给 C,传首元素地址 quzhi(a[0]) 和长度 a.changdu()。取地址之后不要再往数组里追加元素,扩容会让地址失效。

结构体 ​

结构体传指针。字段都是上表里的类型时,内存布局和 C 一致,可以直接传:

cpro
// 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))
    }
}
cpro
// 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))
    }
}
cpro
// 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 类型字段的"镜像"结构体,传之前转换一下:

cpro
// 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))
    }
}
cpro
// 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))
    }
}
cpro
// 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 变量接:

cpro
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)  // 还原成句柄
    }
}
cpro
外联 库 "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)       // 还原成句柄
    }
}
cpro
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:

cpro
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]
}
cpro
外联 函数 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]
}
cpro
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 加进来:

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;
}
cpro
wailian c "shim.c"
wailian hs div_wrap(a z32, b z32, quot daizhi(z32), rem daizhi(z32)) kongbai
cpro
外联 c "shim.c"
外联 函数 div_wrap(a i32, b i32, quot 带址(i32), rem 带址(i32)) void
cpro
extern 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 的样子声明,... 放在最后:

cpro
wailian hs printf(fmt daizhi(w8), ...) z32
cpro
外联 函数 printf(fmt 带址(u8), ...) i32
cpro
extern 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

三条原则 ​

  1. 签名自己负责:编译器相信你写的声明
  2. 内存各管各的:C 返回的指针 cpro 不管释放;c_chuan 借出的指针 C 不能保存
  3. bu_anquan 圈出边界:每个 C 调用都在 bu_anquan 里

cpro 目前处于测试阶段