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

package 与 source import

前两章把单个模块与 C 的边界打通了,本章回答工程化的下一个问题:代码怎么组织成可复用的包。xlang 的答案是一份 JSON 清单 + 一个目录:xlang.package.json 声明包名、版本、源码与依赖,import 一行接入——官方发布的 48 个扩展库(gzip、csv、sqlite、opencv……)和你自己写的包走同一套机制。本章从 import 的三种写法写到多版本共存的真实边界,全部实测。

本章导读

第 33.10 节埋过一个引子:不带 C 别名的 export 进入"包命名空间",消费方用 import mytool 后以 mytool.twice(21) 访问。本章把这个引子展开成完整体系。xlang 的包不是黑盒二进制,而是源码包——清单里列出 .xl 与 .c 源文件,import 时随主程序一起编译(35.5 有 cgen 证据),所以没有"预编译库版本不匹配"的经典灾难。设计哲学三条主线:显式导入不做全局污染(import 进来的符号必须带包名前缀);依赖默认私有(A 依赖 B,import A 的人看不见 B);多版本共存优先于全局唯一版本(不搞 Python 式单环境依赖地狱)。本章依次过:import 的三种写法与命名空间规则、包解析的四级搜索顺序、清单每个字段的含义、自建包从目录到运行的完整流程、用官方 gzip/csv 扩展库各写一个实战、多版本共存的实测能力与当前边界,最后是六条真实报错组成的避坑清单。

本章学习目标

会用 import 的三种形态(包名 / 裸名同目录文件 / 相对路径字符串)并理解各自的命名空间规则;掌握包解析顺序(显式路径 → XLANG_PACKAGE_PATH → release/extlibs)与环境变量"追加而非替代"的实测口径;会写 xlang.package.json 全部字段(xlSources/cSources/dependencies/native.singleton 等);能自建包并完成 import 闭环;会用 release/extlibs 的官方扩展库(gzip 压缩往返、csv 解析实测);理解依赖默认私有、无 re-export 的可见性边界;理解多版本共存的 name@version 目录机制与 packageId 口径,以及当前 xl 层 import 按名命中的实测边界;记住六种 import 相关报错的含义。

35.1 从单文件到包

到现在为止,我们的程序都是"一个 .xl 文件走天下",顶多混编一个 .c。真实项目很快会碰到三个组织问题:一组相关函数想在多个项目间复用;想站在别人的肩膀上(压缩、CSV、数据库)而不是重造轮子;项目大了之后想分层拆分。包机制一次性回答这三个问题,而且把第 32–34 章的混编能力整个收了进来——一个包可以同时携带 .xl 源码和 .c 源码,import 它的人自动获得混编产物。gzip 包就是这个形态的完美示例:xlSources 里是类型安全的 xlang 封装(含参数校验与异常),cSources 里是 XRT 胶水与 zlib 调用,dependencies 里还挂着一个私有的 _zlib 源码包。你在 35.6 会亲手用到它。

35.2 import 三种形态

import 语句有三种写法,对应三种代码来源,全部实测:

写法来源命名空间
import mytool包目录(含 xlang.package.json)或同目录的 mytool.xlmytool
import "./mathx.xl"相对路径的单个 .xl 文件mathx(去路径去扩展名)
import "path_no_ext/probe"相对路径目录下的 probe.xlpath_no_ext.probe(路径变点号)
三种形态实测(均输出 42)
import mathx                  // 同目录 mathx.xl,裸名即可
import "./mathx.xl"           // 同一文件的相对路径写法,二选一
// 用哪个写哪个:mathx.add(19, 23) → 42

import "path_no_ext/probe"    // 官方用例:子目录文件
// 访问带完整路径前缀:path_no_ext.probe.value()

注意三点。一,裸名 import 先在当前文件所在目录找同名 .xl——小项目拆文件不需要任何清单。二,相对路径写法里的 ./ 显式锚定当前文件位置,不做向上查找。三,同一个文件用两种写法各 import 一次会撞命名空间(坑 3)。被 import 的 .xl 文件里,只有 export 的符号可见——mathx.xl 里的 helper 函数没 export,外部摸不到(35.9 展开)。

35.3 解析顺序与环境变量

包名 import 的搜索顺序,设计文档写得明确,本章逐条实测:

  1. 显式相对路径或绝对路径(上节的字符串形态)
  2. XLANG_PACKAGE_PATH 环境变量中的目录(多个用分号分隔)
  3. release/extlibs(官方扩展库目录,内置默认)

实测一:不设环境变量,import gzip 照常工作——release/extlibs 是内置搜索位,不需要任何配置。实测二:把 XLANG_PACKAGE_PATH 设成只含 test/fixtures,import gzip 依然成功——环境变量是追加搜索位,不替代内置位。实测三:不设环境变量 import 自建的 mytool,立即报 cannot resolve import——自定义包仓库必须挂进 XLANG_PACKAGE_PATH。官方测试套件的用法是 set XLANG_PACKAGE_PATH=release\extlibs;test\fixtures,多目录分号分隔、按序命中。

# Windows cmd
> set XLANG_PACKAGE_PATH=D:\mylibs;D:\team\shared
# 之后 xl run/build 都能找到这两个目录里的包

resolver 的克制是设计的一部分:不做向上查找项目文件、不做远程下载、不做完整版本求解器——解析结果完全可预测,"找不到就报错,绝不静默选一个"。

35.4 包清单逐字段

包就是一个含 xlang.package.json 的目录。以本章自建包的完整清单为样本逐字段过:

mytool/xlang.package.json(本章自建,实测可用)
{
    "format": "xlang-package-1",          // 固定值,清单格式版本
    "name": "mytool",                    // 包名 = import 用的名字
    "version": "1.0.0",                 // 包版本
    "abiVersion": "1",                  // ABI 版本(packageId 三元组之一)
    "kind": "demo6-test-package",       // 分类标签(官方扩展库用 demo6-extlib)
    "artifact": {
        "kind": "source",               // 源码包(当前唯一形态)
        "xlSources": ["src/mytool.xl"]  // import 注入的 xlang 源码
    },
    "links": [],                          // 额外链接库(进最终链接)
    "loadlibs": [],                       // 运行期加载的 DLL
    "dependencies": [],                   // 依赖声明(默认私有,35.7/35.8)
    "native": { "singleton": false }    // true = 同进程只允许一个兼容实例
}

几个字段的实测细节。xlSources 是 import 的声明与实现来源;cSources(35.6 的 gzip 用了)是随包编译的 C 源码,包内 .xl 用 extern 对接它们——第 32/34 章的混编被收进了包机制。dependencies 数组每项是 {"name", "version", "abiVersion"} 三元组,编译器在 import 时解析整条依赖链。native.singleton=true 用于"同进程只能存在一份"的 native 资产,两个不兼容实例相遇直接报错(坑 6)。清单里出现未识别字段会被忽略(实测加 "futureField" 照常工作)——这是给未来扩展留的兼容门;但 JSON 本身写坏了,整个包就对 resolver 隐身(坑 1)。

35.5 自建包完整流程

把第 33.10 节的 mytool 引子展开成可复制的四步流程。一,建目录结构:

mylibs/
└── mytool/
    ├── xlang.package.json      # 上节的清单
    └── src/
        └── mytool.xl           # 包源码

二,写包源码,想公开的符号加 export:

src/mytool.xl
export func twice(int x) as int
    return x * 2
end

export int answer = 42

三,挂环境变量:set XLANG_PACKAGE_PATH=...\mylibs(指向 mytool 的父目录)。四,import 使用:

use.xl(实测输出 42 42)
import mytool

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

import 在编译期做了什么?cgen 证据一目了然——use.xl 生成的 C 里,包函数被文本内联进同一个编译单元,名字加了包前缀修饰:

use.c 节选(cgen 实测产物)
long long xl6_export_____xl6__import__mytool__twice(long long x);
static long long __xl6_user_global_____xl6__import__mytool__answer;

没有独立编译单元、没有动态加载——源码包本质上是"带命名空间的静态库",import 就是一次受控的代码展开。这解释了为什么包不需要预编译,也解释了名称修饰如何保证两个包的同名函数不撞车(深入细节 1)。

35.6 使用官方扩展库

release/extlibs 下躺着 48 个发布扩展库,不用任何配置直接 import。挑两个典型的实战胜过千言万语。其一,gzip——把重复文本压缩一个数量级:

usegzip.xl(实测:1350 → 60 → true)
import gzip

func main(...)
    string original = ""
    for i in range(0, 50)
        original += "hello xlang package world! "
    end
    Buffer packed = gzip.compress(original)
    print(string.byteLen(original))   // 1350 字节原文
    print(packed.len())              // 60 字节压缩后
    string back = gzip.decompressText(packed)
    print(back == original)          // true —— 无损往返
end

其二,csv——三行解析一张表:

usecsv.xl(实测输出 3 alice 90)
import csv

func main(...)
    var rows = csv.parse("name,score\nalice,90\nbob,85\n")
    print(rows.length)        // 3(表头也算一行)
    var row = rows[1]
    print(row[0])             // alice
    print(row[1])             // 90
end

使用细节两个:csv.parse 返回动态容器(array<var>),用 var 承接,静态标注 array<array<string>> 会被类型检查拦下;多层下标 rows[1][0] 要拆两行写(第 16 章的索引规则)。扩展库版图一眼看全:压缩族(gzip/bz2/lz4/lzma/zstd/miniz)、归档族(zip/tar)、数据格式(csv/ini/toml/plist/xml 三件套/md4c)、数据库(sqlite/redis)、图像(stb_image 四件套/opencv/dinov3/xfeat/rapidocr)、网络(com/uia/ui.auto、mail 四件套、netrc)、哈希(crc32/md5)、系统(clipboard)……下划线开头的 _zlib/_bzip2/_expat/_libzip/_tomlc17 是私有底层源码包,只被上层包依赖、不直接给你用(35.8 的私有依赖就是这套约定)。每个包的导出接口可以直接读它 src/ 下的 .xl——export 函数列表就是文档。

35.7 依赖与多版本共存

包可以声明依赖,编译器沿链解析。多版本共存是设计的头号原则——"多版本共存优先于全局唯一版本",身份三元组:

packageId 口径(docs/扩展库系统设计.md)
packageId = name@version#abi
// dep@1.0.0#abi1 和 dep@2.0.0#abi1 是不同实例,可以共存

目录层面,同名不同版本的包用 name@version 目录名共存(test/fixtures 里 dep_value 与 dep_value@2.0.0 并排躺在一起)。官方回归用例是本章实测证据——uses_dep 依赖 dep_value 1.0.0(C 函数返回 40),uses_dep_v2 依赖 dep_value 2.0.0(返回 400),同一个程序里同时 import 两个上层包:

mv3.xl(实测输出 42 402)
import uses_dep
import uses_dep_v2

func main(...)
    print(uses_dep.addDep(2))      // 42  —— 走的是 dep_value 1.0.0
    print(uses_dep_v2.addDep(2))   // 402 —— 走的是 dep_value 2.0.0
end

两个版本的 C 源码各自随依赖链编译,互不干扰——没有"全局只能装一个版本"的冲突。版本声明对不上时显式报错(package dependency version mismatch,坑 5),绝不静默降级。

一个必须如实记录的当前边界:本章自建过一套纯 .xl 的镜像实验(left/right 两包分别依赖 dep 1.0.0/2.0.0,值不同),结果两个上层包都拿到 1.0.0 的值——xl 源码层的 import dep 目前按包名命中优先(裸名目录先中),版本三元组的隔离主要体现在 C artifact 编译链上(官方用例的形态)。需要多版本共存时,按官方模式组织:版本差异放在 cSources 里,xl 封装 extern 各自版本的 C 符号。

35.8 依赖默认私有

关键可见性规则:A 依赖 B,import A 的人看不见 B。官方用例一句话验证:

import_dependency_no_pollution.xl(官方负例)
import uses_dep

func main(...) as int
    return dep_value.depValue()     // ← 报错(第 4 行)
end
bad.xl:4:22: [import] private package dependency is not exported

uses_dep 内部可以自由使用 dep_value,但这个"用了谁"的事实不外泄——没有传递性可见,没有自动 re-export。设计文档写得决绝:"如果未来支持 re-export,必须显式声明"。这条规则与 _zlib 命名约定互为表里:gzip 依赖 _zlib 是 gzip 的实现细节,你的程序 import gzip 后看不到 _zlib 的任何符号,依赖升级也就不会震碎下游。想用自己的依赖,自己 import——显式优于隐式。

35.9 export 与可见性

import 的可见性由包内 export 精确划定,三条实测规则收拢前两章的知识。一,只见 export 符号:包源码里没 export 的函数对外不存在——访问报 import package has no such exported symbol(坑 2),包内 helper 因此是天然私有实现。二,不污染全局:import counter 之后直接写 counterAdd(10, 32) 不带前缀,报 undeclared identifier 'counterAdd'(官方负例原文)——符号只活在 counter. 命名空间里。三,带 C 别名的导出双栖:第 33 章说过 export "mytool_add" func add 既进 C 头文件也进包命名空间——import 方用的是 xlang 名字 add,C 宿主用的是 mytool_add,两个世界各取所需。命名空间撞名(同文件两种形态重复 import、或两个来源提供同名命名空间)报 duplicate import namespace(坑 3)。

深入细节

深入细节 1:名称修饰——包隔离的编译期实现

35.5 的 cgen 产物里那串 xl6_export_____xl6__import__mytool__twice 值得细读:xl6_export_ 是导出前缀,___xl6__import__mytool__ 是包路径修饰,twice 才是原名。主程序的符号不戴这顶帽子,两个不同包里的 twice 各戴各的——所以源码内联不会撞名,命名空间隔离在符号层面是物理事实而非约定。代价是生成的 C 可读性下降(调试混编时满屏长名),以及 xl abi dump 里包内符号同样以修饰名出现——审计工具解析 signature 字段里的 fn:app.xxx 形式更稳妥。

深入细节 2:类型身份携带 package instance

设计文档有一条容易滑过去的规则:"类型身份不能只由类型名决定,必须携带 package instance"。含义:dep@1.0.0 的 class Item 和 dep@2.0.0 的 class Item 是两个类型,不能互转——哪怕源码一模一样。这是多版本共存不出类型事故的基石:上层包 A 手里的 Item 永远来自它依赖的那个版本,is/as 判定把 package instance 算进身份。配合"冲突必须显式报错,不能静默选择"的原则,xlang 把依赖地狱的两个入口(版本混用、类型混淆)都在编译期焊死了。

深入细节 3:源码包 vs 二进制包——当前的取舍

为什么 xlang 包是源码形态?设计文档"暂缓项"清单自答:.o/.obj 自动纳入、二进制接口缓存、远程包仓库、semver range 求解、包级 lock 文件全部暂缓。源码包的收益现在就能拿到:import 即混编,永远与当前编译器 ABI 一致,没有预编译地狱;代价是每次编译都重新编译依赖链(大依赖如 opencv 会拖慢编译),以及包源码必须公开。官方把 gzip/sqlite 这种 C 重量级库也以源码形式发布,等于宣告:在二进制缓存落地前,"透明可编译"优先于"编译速度"。读 release/extlibs 里任何包的源码,就是读官方认证的混编最佳实践。

⚠️ 常见错误

⚠️ 坑 1:包找不到(含 JSON 写坏的隐身包)
import nosuchpkg
bad.xl:1:1: [import] cannot resolve import

原因:三种可能——包名拼错;包目录不在搜索路径上(自定义仓库没挂 XLANG_PACKAGE_PATH,35.3 实测三);xlang.package.json 本身 JSON 语法错误(实测:坏 JSON 的包对 resolver 完全隐身,报的是同一条找不到)。排查顺序:先确认包名,再 echo XLANG_PACKAGE_PATH,最后用 json 工具校验清单语法。注意 release/extlibs 是内置搜索位,官方包不需要任何环境变量。

⚠️ 坑 2:访问包内未 export 的符号
    print(mytool.helper())       // helper 没有 export(第 4 行)
bad.xl:4:18: [import] import package has no such exported symbol

原因:import 可见性由 export 精确划定——第 33 章的"包级 export"在这里收口。正确写法:要公开的符号在包源码里加 export;顺手记住没 export 的就是包的私有实现,可以放心重构。

⚠️ 坑 3:同一文件两种形态重复 import
import mathx
import "./mathx.xl"          // ← 报错(第 2 行):同一个文件
bad.xl:2:8: [import] duplicate import namespace

原因:两种写法指向同一命名空间 mathx,编译器拒绝歧义而非静默合并。正确写法:一个文件只选一种 import 形态;团队约定小型项目统一用裸名。

⚠️ 坑 4:隔着上层包摸它的依赖
import uses_dep
    return dep_value.depValue()     // ← 报错(第 4 行)
bad.xl:4:22: [import] private package dependency is not exported

原因:依赖默认私有——uses_dep 依赖 dep_value 是它的实现细节,不传递可见(35.8)。正确写法:需要哪个包,自己 import 哪个包。

⚠️ 坑 5:依赖版本对不上
import uses_dep_bad
bad.xl:1:1: [import] package dependency version mismatch

原因:uses_dep_bad 声明的 dep_value 版本在仓库里不存在——resolver 不做"最接近版本"猜测。正确写法:对齐 dependencies 里的 name/version/abiVersion 三元组与实际发布的包实例;多版本并存用 name@version 目录名(35.7)。

⚠️ 坑 6:两个不兼容的 singleton native 包相遇
import native_one
import native_two          // ← 报错(第 2 行)
bad.xl:2:1: [import] native singleton package conflict

原因:清单里 native.singleton=true 声明"同进程只能存在一个兼容实例"——典型场景是全局状态的 native 库(显卡上下文、全局回调表)。两个不兼容实例同时 import 会在编译期拦下,而不是运行期玄学崩溃。正确写法:二选一,或把两者隔离到不同进程。

本章小结

知识点写法 / 规则要点
import 包import mytool包目录含 xlang.package.json;符号走 mytool. 前缀
import 同目录文件import mathx 或 import "./mathx.xl"二选一,重复 import 报 duplicate import namespace
import 子目录文件import "dir/probe"命名空间带路径:dir.probe.value()
解析顺序显式路径 → XLANG_PACKAGE_PATH → release/extlibsextlibs 内置免配置;环境变量追加不替代
清单核心字段format/name/version/abiVersion/artifact/dependencies/nativexlSources 注入、 cSources 随包编译;未知字段忽略;坏 JSON 包隐身
源码包本质import = 受控代码内联 + 名称修饰无独立编译单元;无预编译地狱
官方扩展库release/extlibs 48 个gzip/csv/sqlite/opencv 等;_ 前缀为私有底层包
依赖私有dependencies 默认私有、无 re-exportimport A 看不见 A 的依赖 B
多版本共存name@version 目录;packageId = name@version#abiC artifact 链实测 42/402;xl 层按名命中优先(当前边界)
可见性只见 export;不污染全局无前缀调用报 undeclared identifier

复习题

  1. import 的三种写法各自适用什么来源?import "path_no_ext/probe" 之后的访问前缀是什么?
  2. 不设 XLANG_PACKAGE_PATH 时,import gzip 成功而 import mytool 失败——解析顺序如何解释这两个结果?
  3. xlang.package.json 里 xlSources 与 cSources 的分工是什么?包内 .xl 如何调用包内 .c 的函数?
  4. "依赖默认私有"是什么意思?uses_dep 依赖 dep_value,为什么 import uses_dep 的程序摸不到 dep_value.depValue()?
  5. 多版本共存的 packageId 三元组是什么?官方 uses_dep/uses_dep_v2 用例实测输出 42 与 402 说明了什么?当前 xl 源码层的边界是什么?

编程练习

练习 1:写一个字符串工具包

自建包 strtool:导出 shout(string) as string(转大写加感叹号)、countWords(string) as int、常量版本号 version = 1。目录结构、清单、XLANG_PACKAGE_PATH、import 使用四步完整走通;再加一个没 export 的内部 helper,验证外部访问它时的真实报错。

参考思路:完全仿照 35.5 的 mytool;字符串大写可用第 18 章的 string.upper。helper 报错预期 import package has no such exported symbol。

练习 2:扩展库三连

用官方扩展库写一个小工具:读入一段文本(硬编码即可),先用 csv.stringify 把它整理成两行三列的表,再用 gzip.compress 压缩,最后打印原文长度、压缩后长度和压缩率。全程只用 import,不配任何环境变量。

参考思路:csv.stringify 接 array<array<string>> 或动态容器;压缩率用 float 除法;gzip.compress 有 string 重载直接收文本。

练习 3:带 C 源码的包

把第 34 章的 counter.c 改造成包 native_counter:清单里 cSources 填 counter.c,xlSources 里用 extern 声明 C 函数组 + 导出 native-backed class Counter(create/add/value/destroy 四件套)。另写主程序 import 使用,并用 counter_closed 计数验证析构。

参考思路:这是 34 章实战 + 本章清单机制的组合题——counter 官方 fixture(test/fixtures/counter)就是标准答案的对照物,写完可以对读它的清单与 src 组织。