本章导读
第 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.xl | mytool |
import "./mathx.xl" | 相对路径的单个 .xl 文件 | mathx(去路径去扩展名) |
import "path_no_ext/probe" | 相对路径目录下的 probe.xl | path_no_ext.probe(路径变点号) |
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 的搜索顺序,设计文档写得明确,本章逐条实测:
- 显式相对路径或绝对路径(上节的字符串形态)
XLANG_PACKAGE_PATH环境变量中的目录(多个用分号分隔)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 的目录。以本章自建包的完整清单为样本逐字段过:
{
"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:
export func twice(int x) as int return x * 2 end export int answer = 42
三,挂环境变量:set XLANG_PACKAGE_PATH=...\mylibs(指向 mytool 的父目录)。四,import 使用:
import mytool func main(...) print(mytool.twice(21)) // 42 print(mytool.answer) // 42 end
import 在编译期做了什么?cgen 证据一目了然——use.xl 生成的 C 里,包函数被文本内联进同一个编译单元,名字加了包前缀修饰:
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——把重复文本压缩一个数量级:
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——三行解析一张表:
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 = 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 两个上层包:
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 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)。
深入细节
35.5 的 cgen 产物里那串 xl6_export_____xl6__import__mytool__twice 值得细读:xl6_export_ 是导出前缀,___xl6__import__mytool__ 是包路径修饰,twice 才是原名。主程序的符号不戴这顶帽子,两个不同包里的 twice 各戴各的——所以源码内联不会撞名,命名空间隔离在符号层面是物理事实而非约定。代价是生成的 C 可读性下降(调试混编时满屏长名),以及 xl abi dump 里包内符号同样以修饰名出现——审计工具解析 signature 字段里的 fn:app.xxx 形式更稳妥。
设计文档有一条容易滑过去的规则:"类型身份不能只由类型名决定,必须携带 package instance"。含义:dep@1.0.0 的 class Item 和 dep@2.0.0 的 class Item 是两个类型,不能互转——哪怕源码一模一样。这是多版本共存不出类型事故的基石:上层包 A 手里的 Item 永远来自它依赖的那个版本,is/as 判定把 package instance 算进身份。配合"冲突必须显式报错,不能静默选择"的原则,xlang 把依赖地狱的两个入口(版本混用、类型混淆)都在编译期焊死了。
为什么 xlang 包是源码形态?设计文档"暂缓项"清单自答:.o/.obj 自动纳入、二进制接口缓存、远程包仓库、semver range 求解、包级 lock 文件全部暂缓。源码包的收益现在就能拿到:import 即混编,永远与当前编译器 ABI 一致,没有预编译地狱;代价是每次编译都重新编译依赖链(大依赖如 opencv 会拖慢编译),以及包源码必须公开。官方把 gzip/sqlite 这种 C 重量级库也以源码形式发布,等于宣告:在二进制缓存落地前,"透明可编译"优先于"编译速度"。读 release/extlibs 里任何包的源码,就是读官方认证的混编最佳实践。
⚠️ 常见错误
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 是内置搜索位,官方包不需要任何环境变量。
print(mytool.helper()) // helper 没有 export(第 4 行)
bad.xl:4:18: [import] import package has no such exported symbol
原因:import 可见性由 export 精确划定——第 33 章的"包级 export"在这里收口。正确写法:要公开的符号在包源码里加 export;顺手记住没 export 的就是包的私有实现,可以放心重构。
import mathx import "./mathx.xl" // ← 报错(第 2 行):同一个文件
bad.xl:2:8: [import] duplicate import namespace
原因:两种写法指向同一命名空间 mathx,编译器拒绝歧义而非静默合并。正确写法:一个文件只选一种 import 形态;团队约定小型项目统一用裸名。
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 哪个包。
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)。
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/extlibs | extlibs 内置免配置;环境变量追加不替代 |
| 清单核心字段 | format/name/version/abiVersion/artifact/dependencies/native | xlSources 注入、 cSources 随包编译;未知字段忽略;坏 JSON 包隐身 |
| 源码包本质 | import = 受控代码内联 + 名称修饰 | 无独立编译单元;无预编译地狱 |
| 官方扩展库 | release/extlibs 48 个 | gzip/csv/sqlite/opencv 等;_ 前缀为私有底层包 |
| 依赖私有 | dependencies 默认私有、无 re-export | import A 看不见 A 的依赖 B |
| 多版本共存 | name@version 目录;packageId = name@version#abi | C artifact 链实测 42/402;xl 层按名命中优先(当前边界) |
| 可见性 | 只见 export;不污染全局 | 无前缀调用报 undeclared identifier |
复习题
- import 的三种写法各自适用什么来源?
import "path_no_ext/probe"之后的访问前缀是什么? - 不设 XLANG_PACKAGE_PATH 时,import gzip 成功而 import mytool 失败——解析顺序如何解释这两个结果?
- xlang.package.json 里 xlSources 与 cSources 的分工是什么?包内 .xl 如何调用包内 .c 的函数?
- "依赖默认私有"是什么意思?uses_dep 依赖 dep_value,为什么 import uses_dep 的程序摸不到 dep_value.depValue()?
- 多版本共存的 packageId 三元组是什么?官方 uses_dep/uses_dep_v2 用例实测输出 42 与 402 说明了什么?当前 xl 源码层的边界是什么?
编程练习
自建包 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。
用官方扩展库写一个小工具:读入一段文本(硬编码即可),先用 csv.stringify 把它整理成两行三列的表,再用 gzip.compress 压缩,最后打印原文长度、压缩后长度和压缩率。全程只用 import,不配任何环境变量。
参考思路:csv.stringify 接 array<array<string>> 或动态容器;压缩率用 float 除法;gzip.compress 有 string 重载直接收文本。
把第 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 组织。