许式伟

Go+ c2go: 近2个月 C 标准库的迁移进展

不知不觉离我们最后一次报告 c2go 的进展又 2 个月过去了。在这两个月里,最重要的当然是 Go+ v1.1 的正式发布。在接下来的 Go+ v1.2 里,我们认为最重要的是这样 3 件事情:

在这两个月里,c2go 继续迭代,并在今天发布了 c2go v0.7.9 及 libc v0.3.11。今天我们就介绍一下这次发布都更新了什么。

c2go v0.7.9:语言更新

我们先来看看 c2go 对语言特性相关的更新。它主要包括:

1、支持了 CompoundLiteralExpr(复合字面量)。简单说,就是支持定义数组常量,结构体常量等。我们以数组常量为代表看个例子就清楚了:

#include <stdio.h>
void foo(unsigned short a[]) {    ...}
void main() {    foo((unsigned short[3]){ 0x330e, 0, 16 });}

结构体(struct)或联合体(union)也是类似的。我们看一个联合体的例子:

#define aslong(v) ((union{double _fval; long _ival;}){v})._ival

这是将一个双精度浮点数转为长整型的例子。这个转换并不是基于数值的,而是基于内存表示的。可以想象,aslong(1.1) 得到的结果是非常古怪的,并不是通过常规的简单数学运算得到。

2、支持 C 内建函数 __builtin_expect。这个关键字是 gcc 引入用来做编译器优化的,这里不详细展开。

3、支持 locale types(本地类型)。在 C 语言中,多个源代码文件允许出现相同名字的类型,但是其具体的类型却并不一致。比如 foo.c 中有 cookie 类型,如下:

struct cookie {    int a;};

bar.c 中也有 cookie 类型,如下:

struct cookie {    double a;    int b;};

在 C 语言中并不会报 cookie 类型重复定义。究其原因,在 C 语言中类型只是内存布局的显式表达,更像是编译器方便我们操作内存的助记符,并不会影响链接器(linker)的链接工作。

4、消除 division by zero 错误(除零错)。在 C 语言中,我们在代码中写 0/0.0 等价于 NaN,写 1/0.0 等价于 Inf。但是如果我们原封不动转为 Go 代码编译阶段就会报 division by zero 错误。

解决思路有两种,一种是把 0/0.0 转为 func() float64 { return 0 }() / 0.0,这样就可以绕过 Go 编译器的 division by zero 检查。另一种则是直接将 0/0.0 转为 math.NaN()。我们选择了前者。

5、消除 static symbol 提前声明导致的链接错误。例如对于如下代码:

static double pone(double);
double f(double v) { return pone(v);}
static double pone(double x) { return 0;}

我们之前版本会把第一个 pone 声明和第二个 pone 实现认为是两个函数,进而导致 f 函数引用的 pone 函数指向第一个 pone 函数并且未实现。

究其原因,是因为在不同文件中声明的同名 static 函数会被 C 认为属于不同的函数,但在之前我们的实现中处理成在同一个文件中多次声明的 static 函数也被认为是不同函数,从而导致了该 bug。

c2go v0.7.9:命令行增强

另外,本次更新 c2go 命令行工具(它等价于 gop c 子命令)也做了不少功能增强,下面一一展开。

1、c2go.cfg 的源代码列表(source)支持忽略文件。在此前,源代码列表支持我们指定目录(dirs)和文件(files),并允许忽略其中的一些函数(ignore.names)不进行编译。

现在,我们新增了忽略文件(ignore.files)功能。这个功能主要是针对我们源代码目录中的文件比较多,我们希望编译其中绝大多数文件,只是对其中的少量文件进行忽略。例如:

{    "source": {        "ignore": {            "files": [                "./src/stdio/puts.c",                "./src/stdio/popen.c"            ]        },        "dirs": [            "./src/math",            "./src/stdio"        ]    },    ...}

这个 c2go.cfg 配置文件告诉编译器去编译 ./src/math 和 ./src/stdio 这两个目录,但是忽略其中的 ./src/stdio/puts.c, ./src/stdio/popen.c 这 2 个文件。

2、c2go.cfg 增加了 template cmds(命令模板)功能。这主要是 libc 的单元测试是以 cmd 形式(可执行程序)存在的,这导致 libc test 工程有非常多的 cmds,为了批量转换这些项目,故引入 template cmds 功能。

常规情况下,c2go.cfg 定义 cmds 的方式是这样的:

{    "target": {        "cmds": [            {                "dir": "cmd/test_qsort",                "deps": [                    "github.com/goplus/libc/test/common",                    "github.com/goplus/libc"                ],                "source": {                    "files": [                        "./src/functional/qsort.c"                    ]                }            },            {                "dir": "cmd/test_string",                "deps": [                    "github.com/goplus/libc/test/common",                    "github.com/goplus/libc"                ],                "source": {                    "files": [                        "./src/functional/string.c"                    ]                }            },            {                "dir": "cmd/test_string_memcpy",                "deps": [                    "github.com/goplus/libc/test/common",                    "github.com/goplus/libc"                ],                "source": {                    "files": [                        "./src/functional/string_memcpy.c"                    ]                }            },            {                "dir": "cmd/test_string_memset",                "deps": [                    "github.com/goplus/libc/test/common",                    "github.com/goplus/libc"                ],                "source": {                    "files": [                        "./src/functional/string_memset.c"                    ]                }            }        ]    },    ...}

引入 template cmds 功能后,它可以改写为:

{    "target": {        "cmds": [            {                "for": [                    "qsort",                    "string",                    "string_memcpy",                    "string_memset"                ],                "dir": "cmd/test_$(it)",                "deps": [                    "github.com/goplus/libc/test/common",                    "github.com/goplus/libc"                ],                "source": {                    "files": [                        "./src/functional/$(it).c"                    ]                }            }        ]    },    ..。}

这让我们在测试案例非常多的时候可以轻松应对。

3、c2go.cfg 增加了 libc、simpleProj 全局标志。这主要是为了适应不同库对 C 内建函数的链接需求。

对于 libc 自身来说,内建函数的实现就在自身的包里面。而对于其他包来说,一般是通过 libc.XXX 来引用。但在少量测试环境下,我们选择人肉实现 C 内建函数,这时我们通过 simpleProj = true 来标识。

4、c2go 命令行增加了 -run 参数用于运行 c2go.cfg 中的某个 cmd。例如对于上面这个例子,我们执行

c2go -run cmd/test_string .

就可以编译并执行 test_string 这个命令了。这对于 libc 的迁移来说非常有用,我们可以单独去执行某种类型的单元测试。

libc v0.3.11:标准库增强

本次 C 标准库的更新最主要的就是进行了大规模的翻译。之前我们 libc 的迁移工作的进度大概只有 5% 左右,当前则预期达到了 20-25% 这个级别。

在本次更新前,libc 的迁移工作是一个文件一个文件来的,我们主要通过它来验证一些典型的函数(例如 printf)迁移没有问题。但现在我们开始以目录为单位进行迁移。本次主要迁移的有:

  • ./src/string(字符串相关,全部完成)

  • ./src/ctype(字符相关,全部完成)

  • ./src/prng(随机数相关,全部完成)

  • ./src/math(数学相关,除 ./src/math/sqrtf.c 外全部)

  • ./src/stdio(标准输入输出,少量文件未完成)

测试案例的迁移当前还较少,主要新增了:

  • github.com/goplus/libc/test/cmd/test/random

  • github.com/goplus/libc/test/cmd/test/strtod

由于新翻译了很多代码但是测试没更少,本次发布 libc 的测试覆盖率从 30% 附近下跌到了 13%。下一步我们将对测试覆盖率进行增强。

欢迎大家通过给 Go+ 和 c2go 代码仓库 Star 来对我们进行支持。相关的代码仓库为:

  • Go+ 源代码:github.com/goplus/gop

  • c2go 源代码:github.com/goplus/c2go

  • C 标准库迁移到 Go:github.com/goplus/libc

另外,也有不少小伙伴私信留言希望能够进 Go+ 用户群参与讨论。如果你感兴趣可以扫描以下二维码加入:

Image