Loment
中文

系统 · 0.1.4.6 起发布

LibL

Loment 的标准库,做成 C 的形状:与 libc 相同的函数名、相同的参数序、相同的 返回值 —— 给已经懂 C 的人用。一个库有两面,不是两个库。

自 0.1.4.6 起可用

自 0.1.4.6 起,发布构建接受 use libl,而且发布构建可以链上 系统真正的 libc.so.6 并跑通到底。0.1.4.5 里挡住这两样东西的那把锁已经打开, 发布说明写明了这一点。

"可用"不是"齐全"。这里的每一个名字都对着系统的 glibc 判过,而缺掉的那一组 写在下面,而不是留给读者在链接那一步才发现。这一页也留着理由,因为一面没有理由的 libc 是没人能审计的承诺。

为什么要有第二面

标准库本来就在,而它一个 libc 名字都没有 —— 没有 strlen, 没有 printf,没有 malloc。那不是一处遗漏,而是一种形状。它的 函数取一个显式的长度,而不是去找结尾符;用一个偏移量寻址进缓冲区;而且从不接受可变数目 的参数。这门语言里没有以 NUL 结尾的字符串,也没有变参函数,所以那个库是照着语言的样子 写成的。

那个形状对写 Loment 的人来说是对的,而对懂 C 的人来说是一道墙。第二面回答的是另一个 问题:要是你从没听说过 Loment,这会叫什么?strcmp,它返回 的东西和 C 的一样。qsort。isdigit。没有东西要学。

两面指向同一批字节。isdigit 调用的是库自己的字符判定; memcpy 调用的是它自己的拷贝例程。所以两面本来会带来的那种失败 —— 同一个字节 因为你问的是哪个名字而给出不同的回答 —— 是结构上不可能,而不只是被避开了。

两种入口

LibL 是这个库的名字;std 仍然是这个包的名字。两个入口都到达 同一个包,区别在于它们各自带进来多少东西。

use std

整个包。LibL 之前写下的一切都已经这么做,而且它继续可用。代价是:一个包级入口把每个 模块拉进一个单元,而这门语言的发射符号空间是平的 —— 于是那些顶层名字必须 彼此唯一,编译时间的增长也快过模块数。

use libl

那四个 libc 面和它们所依赖的东西 —— 六个模块。一个小面:编译得快,而且几乎没有东西会 和它撞名。当你想要的只是 libc 名字时,就用这个入口。

它覆盖什么

一个 libc 头对应一个模块,而这个映射本身就是遵循 libc 的一部分。

<string.h>

strlen、strcpy、strncpy、strcat、 strcmp、strncmp、strchr、strrchr、 strstr、strspn、strcspn、strpbrk、 strtok_r、memchr、memcmp、memcpy、 memmove、memset。

<ctype.h>

isdigit、isalpha、isalnum、 isspace、isupper、islower、 isxdigit、ispunct、isprint,以及这一组里 其余的,外加 toupper 和 tolower。

<stdlib.h>

不需要堆就能构建的那一半:atoi、atol、strtol、 strtoul、abs、labs、llabs、 qsort、bsearch。

<stdio.h>

格式化那一半:snprintf、sprintf,以及其余的 printf 格式。文件那一半在 0.1.4.6 作为单独的 host 模块到来 —— fopen、fclose、fread、fwrite、 fseek、ftell、fflush、feof、 fgets、fputs、puts、putchar、 getchar —— 因为它们要发系统调用,而核心库一个都不发。

再加上一个 libc 没有的函数,而它才是有意思的那个 —— 见下。

对照物就是 libc 本身

标准库其它每一个部分,都是把 Loment 算出的东西和 Python 算出的东西比对来判的。 对这个问题来说那是错的对照物,而且可证:恰恰在定义"行为像 libc"的那些边界上 —— strncpy 会不会补零、strcmp 返回的是一个符号还是一个差、 strchr 会不会找到结尾符、memmove 在区间重叠时是否安全 —— Python 和 libc 给出的不是同一个答案。拿 Python 当对照,会让两份实现一起错、还能通过。

所以这条判据把一个 C 程序对着系统的真 glibc 编译,在两边跑同一组调用,并逐字节 比对标准输出,报出第一处不同的行。有两处归一化是有意为之,否则这个测试量的会是 实现细节而不是语义:标准只承诺 strcmp 的符号,所以符号和差都被检查; 而 glibc 的字符判定返回的是一个位掩码而不是 1,所以两边都在比对之前被归约成一个 是或否。

语言逼出来的东西

这项工作的规则是"libc 怎么实现,LibL 就怎么实现"。这门语言不是 C,所以有四处不得不不同、 有一处不得不新增。每一处都被写下来,而不是藏起来。

字符串字面量不带结尾符

于是对字面量调用 strlen 会读过结尾。在第一次探针里,它给空字符串返回了 17,一路读下去,直到碰巧遇上一个零字节。C 不需要为此写个函数,因为 C 里的 "abc" 本来就带着它的 NUL。这不是一处遗漏 —— 两种 str 类型不是同一个东西:这门语言的带着长度,C 的不带。LibL 加了 cstr_of 把一种转成另一种,而库里每一个 NUL 字符串函数都需要它。

没有变参函数

于是 snprintf 比 libc 的多取一个参数: snprintf(buf, cap, fmt, args, nargs),其中 args 指向一个 64 位值的数组。转换说明符仍然决定每一个怎么读,和 libc 里一模一样。这是库里 唯一一处偏离的签名,而且它不纯粹是损失:libc 数不了自己的参数,给它太少就会 去读栈上碰巧有的东西;这里,太少意味着转换停下,而已经产出的部分被妥当地收尾。

没有函数指针

于是 qsort 的第四个参数是一个枚举,说明按什么比较,而不是一个比较函数。 那是一处真实的差异,而且它在某一个方向上不是降级:它把"这个排序按什么比较"从运行期的 一个地址挪进产物里的一个常量,于是不用读源码就能回答。代价随 它一起被说出来:不是内建几种之一的排序无法表达 —— 你自己写那个循环。

没有模块级可变全局

于是 strtok 变成 strtok_r,游标由调用方持着 —— POSIX 本来 就提供这个,所以这不是降级。一个藏着种子的 rand 也没法存在;这门语言用的 形状从调用方取状态,并且有意地不叫 rand。也没有 errno:strtol 饱和到边界值,然后正常返回,所以调用方改为 对着边界来比。

它不是什么

不是 libc 的替代品

LibL 是一个源码库,不是一个 C 目标文件能链接上去的归档。C 代码没法 调用它;那需要一条"从 Loment 到目标文件"的路,而那条路不存在。它替代的东西更窄,值得 说准确:一个 Loment 程序不得不手写 strcmp。

不是 libc 的全部

realloc 与 free 是有意缺席的:realloc 需要旧 长度,而旧长度住在编译器私有的堆头里;free 这个名字已经被内建占了,库里 再定义一个同名函数,在调用点根本解析不到。malloc 与 calloc 自 0.1.4.6 起在,它们是内建堆上的拼写、自己不带长度头 —— 正因如此内建 free 收得下它们返回的东西。stdin、stdout、 stderr 会是模块级可变全局量,而语言没有这种东西。进程与环境那一族里, 除了 exit 与 abort,一个都没有。而 %f/%e/%g 不是"没做",是"还做不了":上游的 格式化器是软浮点,而它反方向需要一个这一层刻意没有的宽除法。

没完工,也不假装完工

写它的过程暴露出周边系统里的缺陷 —— 其中有:一个辅助函数的重叠检查比的是偏移,而 libc 比的是地址;一个格式化器产出的 IR 被系统汇编器拒绝;一个依赖检查怪错了模块。这些都被 开成 issue,而不是顺手修掉,而这个库自己的判据有意地窄:它声称这些模块是合法单元,而 不声称它们的依赖也是。