第 33 章 / 共 58 章 卷七 系统与互操作

export 与 ABI 元数据

第 32 章解决了"xlang 调 C",本章把镜头反转:C 调 xlang。一行 export "c_symbol",函数、变量、常量、类方法就变成 C 头文件里堂堂正正的声明;xl abi 把整个模块的对外契约 dump 成文本或 JSON,签字画押、随版本存档。本章从最小导出写到 C 宿主完整闭环,多返回的 out 指针形态、class 实例的 xvalue 句柄交接、DLL 导出表的真实现状,全部实测一遍。

本章导读

互操作是一条双行道。extern 把 C 符号拉进 xlang 的世界,export 则把 xlang 符号推出去——推向两个方向:一是 C ABI,带 "c_symbol" 别名的导出会出现在 cgen 生成的头文件里,C 宿主 #include 之后像调普通 C 函数一样调你的 xlang 代码;二是 xlang 包命名空间,不带别名的 export 不进 C 头文件,但能被别的 xlang 模块 import 后使用。两个方向共用同一个关键字,差别只在有没有那个字符串别名。本章逐形态展开:函数导出(含"纯数值导出无需初始化运行时"的实测)、变量/常量导出的四种写法、多返回如何变成 out 指针、class 成员导出的 self 首参与 xvalue 句柄交接,然后用 xl abi 把整个对外契约 dump 出来逐字段解读,最后跑通 C 宿主静态混编闭环,并如实记录 -shared DLL 导出表在当前的缺口。读第 32 章时建立的 ABI 类型、位宽、混编知识在本章全部复用。

本章学习目标

会写 export "c_symbol" 导出函数并用 cgen --emit/--emit-header 生成 .c/.h;掌握 export 变量/常量的四种形态(带别名进 C ABI、不带别名仅包级、const、export extern 再导出);理解多返回导出变为 out0/out1 输出指针(可传 NULL);会导出 class 成员(首参 XL_Class_X* self)并理解类实例以 xvalue 句柄跨边界(xvoGetRecordData 解包、xvoUnref 释放、xrtInit/xrtUnit 配对);会用 xl abi(文本与 --json)审读模块对外契约;会用 tcc 把 cgen 产物与 C 宿主静态混编成 exe;知道 -shared DLL 当前无导出表的现状与 --loadlib 的适用边界;知道同名 .c 会被自动编入混编;理解包级 export 与 import 的消费关系。

33.1 export 是 extern 的镜像

ABI 设计文档把这对关键字称为对称声明模型:extern 让 C 符号进入 xlang,export 让 xlang 符号进入 C。写法上也是镜像的——extern 后面的字符串是"已存在的 C 符号名",export 后面的字符串是"要给 C 用的符号名":

对称声明模型(docs/ABI设计.md §8/§9)
extern "c_symbol" func xl_name(...) as Type    // C → xlang(第 32 章)
export "c_symbol" func xl_name(...) as Type    // xlang → C(本章)

三条硬规则来自设计文档原文且都经本章实测:C export symbol 完全等于字符串内容,不做任何编码改名,所以头文件里的符号稳定、可读;同一模块中不允许两个导出共用同一个 C 符号(坑 2 有真实报错);C export symbol 不参与 xlang 重载解析——重载函数要导出给 C 时,每个重载必须配一个不同的 C 符号。第 32 章的实战里我们已经用过一次 export("xl6_double_it" 让 C 回调 xlang),本章把全貌展开。

33.2 export 函数与头文件

最小导出模块:一个带别名的导出函数,外加一对带别名的导出变量/常量,以及一对不带别名的包级导出——这个 lib.xl 是本章前半段的主角:

lib.xl(本章实测主角)
export "app_add" func add(int a, int b) as int
    return a + b
end

export "app_counter" int counter = 41
export "app_version" const int version = 6

export int packageCounter = 1        // 无别名:仅包级导出
export const int packageVersion = 6

cgen 加 --emit-header 一次产出 .c 与 .h:

$ xl cgen lib.xl --emit lib.c --emit-header lib.h
# 无输出即成功

生成的 lib.h 全文值得逐行读(运行时声明部分此处折叠):

lib.h(cgen 实测产物,运行时声明略)
#ifndef XL6_LIB_H
#define XL6_LIB_H

#include <stdbool.h>
#include <stdint.h>
#include <stddef.h>

/* …… xvalue、XL6_Function 结构、xlInit/xlUnit、
   xlDllAttach/xlDllDetach 等运行时声明 …… */

#ifdef __cplusplus
extern "C" {
#endif

int64_t app_add(int64_t a, int64_t b);
extern int64_t app_counter;
extern const int64_t app_version;

#ifdef __cplusplus
}
#endif

#endif /* XL6_LIB_H */

三个观察。一,类型映射严格落地:xlang int → C int64_t,与第 32 章的映射表完全互逆。二,带别名的导出全部到场:app_add 是函数原型,app_counter/app_version 是 extern 变量声明(const 保留)。三,不带别名的 packageCounter/packageVersion 消失了——它们只进入 xlang 包命名空间,不生成 C 头文件声明(33.6 的 ABI dump 会再次印证这一点,设计文档原文:"没有显式 C 符号的 export 只进入 xlang 模块导出,不生成 C 头文件声明")。

另一个实测细节:纯数值导出不需要初始化运行时。官方用例 export_add_header_host.c 的 main 只有一行 return xl6_add_i64(20, 22) == 42 ? 0 : 1;,没有 xrtInit、没有 xlInit——因为加法不碰 xvalue、不碰堆。一旦导出涉及 class 实例或运行时资源,初始化就成了义务(33.5)。

33.3 export 变量与常量

功能清单 §15 列出的变量/常量导出写法共四种,本章逐一实测:

写法进 C 头文件?实测产物 / 说明
export "sym" int x = 41✅extern int64_t sym;(.c 里有定义与初值)
export "sym" const int x = 6✅extern const int64_t sym;
export int x = 1(无别名)❌仅包级导出,.c 里是 static 内部全局
export extern "sym" int x✅(声明)不拥有存储,把 C 全局变量以公开符号再导出

float 与 bool 同样能导出,类型映射延续第 32 章口径——实测:

v3.xl 与生成头文件摘录(实测)
export "app_pi" float pi = 3.5
export "app_rate" const float rate = 1.5
export "app_flag" bool flag = true

// 头文件得到:
// extern double app_pi;
// extern const double app_rate;
// extern bool app_flag;

两个写法陷阱。一,var 在变量声明里只表示"动态类型",不是 extern/export 的标记词——export "sym" var int x = 1 这种旧写法已不属于 DEMO6(实测直接语法错误);动态类型变量带 C 别名更会被语义检查拦下(坑 3)。二,export extern 是无初始化器的纯声明,生成的 C 里就是一行 extern int64_t host_tick;——存储由某个 C 源提供,xlang 侧读写都落到那个 C 全局上。它同时把变量公开进包命名空间,import 方也能看到。

33.4 多返回导出:out 指针

xlang 函数可以返回多个值(第 12 章),C 没有这个概念——导出时的翻译策略是经典做法:返回值变 void,每个返回值追加一个输出指针。实测模块(geom.xl 的一部分):

多返回导出(geom.xl 节选,实测)
export "app_pair" func pair(int seed) as (int, int)
    return seed, seed + 1
end

// 头文件得到:
// void app_pair(int64_t seed, int64_t* out0, int64_t* out1);

生成的 wrapper 内部值得看一眼——先调内部函数拿到打包的返回结构体,再逐个字段写进 out 指针,并且判空:

geom.c 中的 wrapper(cgen 实测产物)
void app_pair(int64_t seed, int64_t* out0, int64_t* out1)
{
    XL6_Function_Return_A87320477CD23C4A __xl6_return = xl6_export_pair(seed);
    if ( out0 != 0 ) {
        *out0 = (int64_t)__xl6_return.value0;
    }
    if ( out1 != 0 ) {
        *out1 = (int64_t)__xl6_return.value1;
    }
}

两个工程含义:C 调用方对不关心的返回值可以传 NULL;内部返回结构体的名字带哈希后缀(XL6_Function_Return_ + 16 位大写十六进制),这是内部 ABI 符号的命名规则,与 C 侧无关——C 侧只看到干净的 void + out 指针 签名。33.7 的宿主实战里 app_pair(9, &a, &b) 正确取回 (9, 10)。

33.5 class 成员导出

xlang 成员函数只有显式 export 才进入 C 公开 API。把 33.4 的 geom.xl 补全——一个带构造器、两个导出方法的 Point 类,加一个返回类实例的导出工厂:

geom.xl(全文,本章实测)
class Point
    int x
    int y

    create(int x0, int y0)
        self.x = x0
        self.y = y0
    end

    export "app_point_move" func move(int dx, int dy) as int
        self.x = self.x + dx
        self.y = self.y + dy
        return self.x + self.y
    end

    export "app_point_pair" func pair() as (int, int)
        return self.x, self.y
    end
end

export "app_point_make" func makePoint(int x, int y) as Point
    return Point(x, y)
end

头文件里这组声明把 class 跨边界的整套规则说完了:

geom.h 导出区(实测产物)
typedef struct XL_Class_Point {
    int64_t x;
    int64_t y;
} XL_Class_Point;

int64_t app_point_move(XL_Class_Point* self, int64_t dx, int64_t dy);
void app_point_pair(XL_Class_Point* self, int64_t* out0, int64_t* out1);
xvalue app_point_make(int64_t x, int64_t y);

三条规则逐一对应。一,成员导出的首参是实例指针 XL_Class_Point* self(设计文档原文:"C export wrapper 的第一个参数是实例指针,static 方法没有该参数");多返回成员再叠加 out 指针。二,类的字段布局公开在头文件里——XL_Class_Point 就是普通 C 结构体,C 侧甚至可以 data->x 直读字段(33.7 实测输出 7)。三,顶层函数里的类实例走 xvalue 句柄:makePoint 返回的是不透明的 xvalue,不是结构体指针——xvalue 带引用计数,是运行时的"户口本";结构体指针只是"门牌号"。从句柄到门牌号的解包函数是 xvoGetRecordData,句柄用完要 xvoUnref 归还。这套交接涉及运行时,所以 C 宿主必须 xrtInit()/xrtUnit() 配对。完整调用套路 33.7 用真实宿主程序演示。

33.6 xl abi:ABI 元数据

头文件是给 C 编译器看的契约,xl abi 是给人和工具看的契约——把模块的对外符号表 dump 成文本或 JSON:

$ xl abi lib.xl --emit lib.abi.txt
$ xl abi lib.xl --json --emit lib.abi.json
lib.abi.txt(实测全文)
xlang abi version: 6
module: app
export app_add
  name: add
  signature: fn:app.add(int,int) as int
  symbol: xl6_export_add
  c_export: app_add
export app_counter
  name: counter
  kind: var
  type: int
  c_export: app_counter
export app_version
  name: version
  kind: const
  type: int
  c_export: app_version
export
  name: packageCounter
  kind: var
  type: int
  c_export:
export
  name: packageVersion
  kind: const
  type: int
  c_export:

逐字段解读。signature 是 xlang 侧的全名签名(模块名.名字(参数) as 返回);symbol 是内部 ABI 符号——cgen 生成的 C 里真正实现逻辑的函数名(深入细节 1 讲两层结构);c_export 才是写进头文件、给 C 用的公开符号。注意最后两条:不带别名的包级导出 c_export 为空——"只进包命名空间、不进 C ABI"在元数据层面一目了然。

class 成员导出的记录多一个 owner 字段(JSON 里 kind 为 class_method):

geom.abi.txt 节选(实测)
export app_point_move
  owner: Point
  name: move
  signature: fn:app.Point.move(int,int) as int
  symbol: xl_class_Point__move
  c_export: app_point_move

extern 声明同样入 dump(字段名 c_symbol),所以一份 abi 文件就是模块的完整进出账。JSON 版适合工具消费,一条记录一行事实:

lib.abi.json 节选(实测)
{"kind":"export_func","name":"add","signature":"fn:app.add(int,int) as int",
 "symbol":"xl6_export_add","cExport":"app_add"}

两个边界要记。一,dump 的覆盖面:只有 C ABI 导出符号——export 函数/变量/常量、extern 声明、类的 export 方法;class 字段布局、union 定义、derive 关系、protocol、#doc 文档块这些 richer 元信息当前不进 abi dump。二,旧命令 xl xapi 已随 .xapi 产物一起退役(实测:unknown command: xapi),接口元数据现在由 abi 阶段统一承载。另外 xl run --save-temps 会在临时目录里留下 module.abi.json,调试混编时顺手可查。

33.7 C 宿主静态混编闭环

理论够了,把 geom.xl 真正交给一个 C main 驱动。宿主程序覆盖全形态:工厂函数拿句柄、解包、调成员导出、直读字段、多返回 out 指针:

geom_host.c(实测通过)
#include <stdio.h>
#include "xrt.h"          /* XRT 运行时:xrtInit/xvalue/xvoGetRecordData */
#include "geom.h"         /* cgen 生成的导出契约 */

int main(void)
{
    int64_t x = 0, y = 0, a = 0, b = 0;
    xvalue point;
    XL_Class_Point* data;

    if ( xrtInit() == NULL ) {
        return 1;
    }
    point = app_point_make(2, 3);                    /* xvalue 句柄 */
    data = (XL_Class_Point*)xvoGetRecordData(point); /* 解包 */
    printf("move(5,7) = %lld\n", (long long)app_point_move(data, 5, 7));
    app_point_pair(data, &x, &y);
    printf("pair = (%lld, %lld)\n", (long long)x, (long long)y);
    printf("direct field x = %lld\n", (long long)data->x);
    app_pair(9, &a, &b);
    printf("app_pair(9) = (%lld, %lld)\n", (long long)a, (long long)b);
    xvoUnref(point);                                  /* 归还句柄 */
    xrtUnit();
    return 0;
}

编译链路:cgen 产物 + 宿主 + XRT 目标文件,交给 xl 自带的 TCC 一把过(测试套件 test/suites 里每个官方 export 用例走的就是这条路):

$ xl cgen geom.xl --emit geom.c --emit-header geom.h
$ tcc -I. geom.c geom_host.c xrt.o libtcc1.a \
      -lWs2_32 -lIPHLPAPI -lShell32 -o geom_host.exe
$ ./geom_host.exe
move(5,7) = 17
pair = (7, 10)
direct field x = 7
app_pair(9) = (9, 10)

输出全部符合预期:move 返回新坐标之和 17,pair 拆出 (7, 10),字段直读 7,顶层多返回 (9, 10)。这个闭环里 xlang 完全退到"库"的位置——main 在 C 侧,xlang 代码经 cgen 变成普通 .c 参与编译,运行时靠 xrtInit/xrtUnit 手动拉起。第 38 章嵌入 C 宿主会在这条路上走得更远(xl.dll 动态 API),静态混编是最朴素也最透明的一种。

33.8 DLL、impdef 与 --loadlib

静态混编之外,直觉的下一步是编成 DLL 给宿主动态链接。xl build -shared 确实能产出 DLL:

$ xl build geom.xl -shared -o geom.dll --emit-header geom_shared.h
# 成功,geom.dll 约 500 KB
$ xl impdef geom.dll -o geom.def
tcc: impdef: no symbols found in 'D:\GIT\x-lang\demo6\tmp\bookch\geom.dll'

这是本章必须如实记录的现状:当前版本生成的导出 wrapper 没有 __declspec(dllexport) 标注,-shared 产出的 DLL 导出表是空的——impdef 找不到符号,C 宿主直接链接报 unresolved reference to 'app_point_make' 等一串,xlang 侧 xl run main.xl --loadlib geom.dll 同样在链接阶段失败。官方回归也只验证 DLL 文件与头文件"生成出来",没有消费它的导出。换句话说:-shared 当前的主要价值是和 --emit-header 一起产出契约文件;真正可用的 C 宿主闭环是 33.7 的静态混编,或者把 cgen 的 .c 交给你自己的构建系统、自行补导出声明后编 DLL。

那 xl impdef 和 --loadlib 是给什么用的?给带 dllexport 的 C 侧 DLL 用的——这正是第 32 章 extern 的逆向消费场景。实测一条完整闭环:C 源码显式导出两个符号,tcc 编成 DLL,impdef 抽出 def,xlang 侧 extern + --loadlib 直调:

cdll.c(C 侧显式导出)
__declspec(dllexport) long long app_add(long long a, long long b)
{
    return a + b;
}
__declspec(dllexport) long long app_counter = 41;
$ tcc -shared cdll.c -o cdll.dll
$ xl impdef cdll.dll -o cdll.def
$ type cdll.def
LIBRARY cdll.dll

EXPORTS
app_add
app_counter
$ xl run main.xl --loadlib cdll.dll
42

impdef 产出标准 PE 模块定义文件格式(LIBRARY + EXPORTS 段),--loadlib 内部正是靠它对 DLL 做符号解析。闭环成立——但方向是"C 写的 DLL 给 xlang 用",不是"xlang 写的 DLL 给 C 用"。后者等导出表支持落地后再补课。

33.9 同名 .c 自动混编

本章实测中撞出一条此前没有明写的规则:xl run/build x.xl 会自动把同目录下同名的 x.c 当作混编 C 源一起编译,不需要任何参数。验证:

auto.xl 与 auto.c(同目录同名,实测)
// auto.c
long long auto_answer(void) { return 42; }
extern "auto_answer" func autoAnswer() as int

func main(...)
    print(autoAnswer())     // xl run auto.xl 输出 42——没传 auto.c 也链上了
end

这是便利也是陷阱。便利:一个 .xl 配一个同名 .c 是混编最自然的组织方式,零参数。陷阱:如果你先 cgen geom.xl --emit geom.c 留了产物,再 xl build geom.xl,这份 geom.c 会被自动编入——里面的符号与 build 自己生成的代码重复,链接阶段炸出一串 defined twice(坑 4 有真实报错)。规避方法很简单:cgen 产物移出源码目录,或与 .xl 不同名。

33.10 包级 export 与 import 一瞥

不带别名的 export 去哪了?答案在 import 一侧。自建最小包验证消费关系——包目录里放清单 xlang.package.json 与源码:

mytool/src/mytool.xl(包内源码,实测)
export "mytool_add" func add(int a, int b) as int   // 同时进 C ABI 与包空间
    return a + b
end

export func twice(int x) as int                       // 仅包空间
    return x * 2
end

export int answer = 42                               // 仅包空间
main2.xl(消费方,实测通过)
import mytool

func main(...)
    print(mytool.twice(21))     // 42
    print(mytool.answer)       // 42
end

要点三个。一,消费方用 import 包名 后,以 包名.符号 访问——没有 import 方的全局污染。二,带 C 别名的导出(mytool.add)同样能从包空间访问,别名只影响 C 侧,不影响 xlang 侧的名字。三,import 解析依赖 XLANG_PACKAGE_PATH 环境变量指向包仓库目录(官方测试用 test/fixtures,本书实测用自建目录);包清单声明 format/name/version/artifact 等字段。缺包与访问未导出符号各有专属报错(坑 5、坑 6)。package 的完整机制——版本、依赖、native 包、extlibs——是第 35 章的正题,本章只需建立"包级 export 的消费方是 import"这个对口关系。

深入细节

深入细节 1:两层符号——wrapper 与内部实现

cgen 生成的 C 里,每个导出函数其实有两个符号。内部实现叫 xl6_export_add(xl6_export_ + xlang 名),用内部 ABI(long long、打包返回结构体);公开 wrapper 才叫 app_add,用严格的 C ABI 类型,函数体只有一件事——ABI 转换后直转内部函数:

int64_t app_add(int64_t a, int64_t b)
{
    return xl6_export_add(a, b);
}

这就是 abi dump 里 symbol(内部)与 c_export(公开)两个字段的由来。分层的好处:xlang 侧重载、改名、签名演化只影响内部符号(重载时内部符号带哈希后缀区分),公开契约保持稳定——设计文档的原话是"C export symbol 是公开契约,不能随 internal ABI symbol 变化自动改名"。所以给 C 用的符号名请当 API 设计:语义化、带模块前缀(app_、mytool_),别随手叫 add。

深入细节 2:导出的类型面——什么能过边界

本章实测拼出的可用性矩阵:数值(int/float/bool 及各 c_* 类型)函数参数、返回值、变量、常量全通;string 参数可以(按 const char* 传入),返回值被拒(坑 1);string/动态类型变量带 C 别名被拒(坑 3);class 实例作为参数或返回值时走 xvalue 不透明句柄(33.5);多返回翻译为 out 指针。编译器报错里出现了"第一阶段""第二阶段"两种措辞——变量检查报 first-stage(英文消息),返回值检查报 第二阶段(中文消息),是编译器消息风格尚未统一的小瑕疵,含义都是"该类型不在 C ABI 直通的类型集内"。记忆法:能精确对应 C 类型的才直通;运行时才存在的(string 堆对象、动态 var)要么降维成指针参数,要么包装成 xvalue 句柄。

深入细节 3:XL_Class_Point 为什么不是黑盒

一个反直觉的设计:class 字段布局直接公开在头文件里(typedef struct XL_Class_Point { int64_t x; int64_t y; }),C 侧 data->x 直读合法且实测可用。这与"句柄不透明"并不矛盾,而是分工:xvalue 管生命周期(引用计数、xvoUnref 归还、GC 可见),XL_Class_Point* 管数据访问(零开销直读直写)。C 侧只要不缓存指针超过句柄存活期(先 xvoUnref 再用指针就是悬垂),两个视角可以安全混用。这也解释了为什么成员导出首参是结构体指针而非 xvalue——方法体操作的是数据,不是生命周期。需要完全隐藏字段的场景属于第 34 章 native-backed class 的透明 C payload 议题。

⚠️ 常见错误

⚠️ 坑 1:导出 string 返回值被语义检查拦下
export "xl_greet" func greet(string name) as string
    return "hello, " + name
end
bad.xl:1:46: [sem] 该返回类型不能直接进入第二阶段 C ABI export

原因:xlang string 是运行时堆对象,C 侧没有对应的拥有权语义,返回值跨边界无法安全交接;而 string 参数可以降维为 const char* 传入,所以只拦返回方向。正确写法:返回数值/布尔等直通类型;字符串结果让 C 侧传缓冲区,或包进 class 用 xvalue 句柄带出(33.5)。

⚠️ 坑 2:两个导出共用同一个 C 符号
export "dup" func a() as int
    return 1
end
export "dup" func b() as int      // ← 报错定位在这一行(第 4 行)
    return 2
end
bad.xl:4:8: [sem] 重复的 C export symbol

原因:C export symbol 完全等于字符串内容、不做编码,同名即冲突——重载函数也不例外,C 没有重载。正确写法:每个导出配唯一符号,重载按签名区分命名,如 math_add_i64/math_add_f64。

⚠️ 坑 3:动态类型变量带 C 别名导出
export "app_msg" var msg = "hello"
bad.xl:1:18: [sem] variable type cannot enter first-stage C ABI directly

原因:var 声明的是动态类型变量(可装任何值),C 侧没有对应物;初值是 string 时同理。注意这条报错是英文而坑 1 是中文——编译器消息风格尚未统一,看到 first-stage/第二阶段 都是同一个意思:类型不在 C ABI 直通集内。正确写法:给明确的静态类型(int/float/bool);string 内容留在包级导出(无别名),不进 C ABI。

⚠️ 坑 4:cgen 产物留在原地,build 报 defined twice
$ xl cgen geom.xl --emit geom.c
$ xl build geom.xl -shared -o geom.dll
tcc: geom.c:1010: error: 'app_pair' defined twice

原因:xl run/build x.xl 会自动编入同目录同名 x.c(33.9)——cgen 留下的 geom.c 与 build 自己生成的代码符号重复。正确写法:cgen 产物移出源码目录或改名;这条自动混编规则用对了是便利,忘了就是地雷。

⚠️ 坑 5:-shared DLL 链接宿主报 unresolved reference
$ xl build geom.xl -shared -o geom.dll
$ tcc geom_host.c geom.dll ... -o host.exe
tcc: error: unresolved reference to 'app_point_make'
tcc: error: unresolved reference to 'app_point_move'
……

原因:不是链接姿势不对,是当前版本生成的导出 wrapper 没有 dllexport 标注,DLL 导出表为空——xl impdef 会直接告诉你 no symbols found(33.8)。正确写法:眼下 C 宿主走 33.7 静态混编;--loadlib 只适用于带 __declspec(dllexport) 的 C 侧 DLL。

⚠️ 坑 6:import 的两种专属报错
import nosuchpkg           // 包不存在或不在 XLANG_PACKAGE_PATH 上
bad.xl:1:1: [import] cannot resolve import
    print(mytool.helper())   // helper 没有 export(第 4 行)
bad.xl:4:18: [import] import package has no such exported symbol

原因:import 的可见性完全由包内 export 决定——没 export 的符号对 import 方不存在;包解析依赖 XLANG_PACKAGE_PATH 指向包仓库。正确写法:要公开的符号加 export(无别名即可);运行前确认包路径环境变量。

本章小结

知识点写法 / 命令要点
导出函数export "sym" func f(...) as T进 C 头文件;C 符号=字符串原文;全模块唯一
导出变量/常量export "sym" int x = 1 / export "sym" const int v = 6头文件为 extern 声明,const 保留;float→double、bool→bool
包级导出export int x = 1(无别名)只进 xlang 包命名空间,不进 C 头文件;abi dump 中 c_export 为空
export externexport extern "sym" int x不拥有存储;把 C 全局再导出到包命名空间
多返回导出as (int, int)C 侧变 void + out0/out1 输出指针,可传 NULL
class 成员导出类内 export "sym" func m(...)首参 XL_Class_X* self;字段布局公开在头文件
类实例跨边界xvalue + xvoGetRecordData + xvoUnref句柄管生命周期,结构体指针管数据;宿主须 xrtInit/xrtUnit
纯数值导出—不碰运行时,C 宿主可免初始化直调
ABI 元数据xl abi f.xl [--json] [--emit out]枚举 export/extern/类导出方法;symbol=内部、c_export=公开
静态混编闭环cgen + tcc 编 .c+宿主+xrt.o当前最可靠的 C 宿主路径
-shared DLLxl build -shared -o x.dll当前导出表为空;impdef 报 no symbols found
impdef / --loadlibxl impdef x.dll -o x.def面向带 dllexport 的 C 侧 DLL;def 为 LIBRARY+EXPORTS 格式
同名 .c 自动混编x.xl 旁的 x.c 零参数编入便利;cgen 产物同名遗留会 defined twice
import 消费import pkg 后 pkg.sym只见 export 符号;包机制详见第 35 章

复习题

  1. export 后面的字符串别名有哪三条硬规则?(提示:内容、唯一性、重载)
  2. export int counter = 1 与 export "app_counter" int counter = 1 在头文件和 abi dump 中各有什么差别?
  3. 多返回函数 func pair(int seed) as (int, int) 导出后 C 侧签名是什么?为什么不关心的返回值可以传 NULL?
  4. C 宿主调用 class 成员导出前,从 app_point_make 拿到的 xvalue 要经过哪两步才能调 move?用完要做什么?为什么纯数值导出可以跳过这一切?
  5. 为什么 xl build -shared 产出的 DLL 当前无法被 C 宿主链接消费?xl impdef 的正确使用对象是什么?

编程练习

练习 1:导出一个迷你数学库

写 mathx.xl:导出 mathx_add/mathx_mul 两个函数、mathx_pi 常量(float)、mathx_call_count 变量(int,每次调用自增)。用 cgen 生成 .c/.h,检查头文件中四个符号的类型,再写一个 C main 宿主静态混编验证调用计数生效。

参考思路:函数体里 callCount = callCount + 1 即可——导出变量在 .c 里是真实全局,C 侧 printf 读 mathx_call_count 能看到增长。注意宿主不碰运行时资源时可免 xrtInit。

练习 2:Counter 类的 C 侧操纵

仿照 33.5 的 Point,实现 class Counter:create(int start)、导出方法 counter_incr(自增并返回新值)、counter_get(返回当前值)、顶层工厂 counter_make。写 C 宿主:造两个实例,交替 incr,最后用字段直读核对值,并正确归还两个句柄。

参考思路:每个 xvalue 各解包一次、各 xvoUnref 一次;两个实例的 XL_Class_Counter* 互不干扰——这正是首参 self 的意义。

练习 3:用 xl abi 审计模块契约

把练习 1 的 mathx.xl 和 33.5 的 geom.xl 各跑 xl abi --json,用第 26 章的 json 标准库(或任何脚本)写一个审计工具:列出所有 cExport 非空的符号及其 kind,并检查是否有重复 cExport、是否有"忘了加别名"的包级导出(cExport 为空)。

参考思路:JSON 的 symbols 数组里每条记录的 cExport 字段即判断依据;重复的 cExport 其实过不了编译(坑 2),所以审计重点是空 cExport 清单——它回答"这个模块有哪些符号只给 xlang 用"。