背负青天而莫之夭阏者,而后乃今将图南。
百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 互联网 > 行业动态 > 正文

Go+ 下个里程碑:超越 cgo,无缝对接 C 语言

swdejp 2023-10-03 154 浏览 0 评论

去年(2021年)Go+ 的 slogan 从 “面向数据科学” 的语言升级到了 “面向工程、STEM 教育与数据科学” 三位一体的语言。也就是说,我们希望 Go+ 可以同时被软件工程师、中小学生、数据分析师这三个截然不同的人群所广泛使用。

对 Go+ 来说,“面向数据科学” 这个目标注定有非常长远的路要走。所以去年 Go+ 的版本迭代主要精力都花在了 “低门槛” 上。我们努力让 Go+ 的使用门槛低到和 Python 相当的水平。这是从 Go+ 作为 “面向 STEM 教育”,作为一种中小学生就能够学习和掌握的教学语言而做出的努力。我认为这些努力对 Go+ 的发展来说非常重要。2022 年我们仍然会继续去加强 Go+ 在低门槛方向上的工作。

但是在去年年底的时候,我们将 Go+ 下一个里程碑(v1.2)的版本计划做了非常巨大的调整:我们从之前的一箩筐的 Go+ 生态发展的计划,调整成为就干一个非常硬的硬骨头:实现对 C 语言的完美支持。

为什么需要做这个改变?

因为到目前为止,Go+ 从 “面向工程” 这个目标来说,它只是 “更好的 Go”。客观来讲,这个 “更好” 并不足以让软件工程师们心动到改变自己的习惯,采用 Go+ 来进行日常的开发工作。这个 Why Go+ 如果回答不好,那么 Go+ 的面向工程这个目标就仅仅停留于口号。

Go 语言足够好,在它擅长的服务端开发领域上,你甚至几乎可以肯定地说它是最好的。但是在其他所有领域,无论是 PC、Mobile、Web、小程序、嵌入式、区块链与 Web3、大数据与 AI、编程教学领域等等,Go 都显得不够好。如果用学生来打比方,Go 像是一个偏科偏的非常严重的学生,有一门学科特别擅长考 99 分,但是其它学科都只是勉强的及格线。

是什么制约了 Go 语言在其他领域的发展?

我们以 Web3 为例。这很可能是与服务端开发最为接近,Go 最容易取得压倒性优势的领域。但是实际的战绩如何呢?我花时间对几十个头部的 Web3 项目进行了分析,最后的统计结果很意外:在这个领域 Go 语言的确采用率很高的确没错,但是有另一个语言 Rust 的采用率也很高,两者的采用率基本上接近 1:1,甚至一些项目同时采用 Go 和 Rust。

为什么会这样?在正统的服务端开发中 Rust 的采用率可能连 Go 的 1/10 都没有,为什么一个看起来完全类似的同样都是网络应用类的场景,其采用率居然有这么大的差别?

我知道一些人把这个事情归因到 Go 是 GC 语言,Rust 性能更好这一点上。但我认为这并不成立。如果对网络服务性能是重要的,那么在服务端开发(尤其是云计算)这样一个成本很敏感的领域,Rust 应该比 Go 更有市场竞争力才对。

我认为最有可能的关键因素是两个:

  • 语言带给人的安全感(更少的安全漏洞);

  • C 语言的兼容性。

Rust 的确能够带来更好的安全感(至少表面上看是这样),但这个因素能够影响到底有多少不得而知。但我认为 C 语言的兼容性是更为重要的因素。Web3 是一个快赛道,发展日新月异,大家都会抢时间。从抢时间这个角度来看,只对比语言角度 Go 比 Rust 有优势得多。是什么原因让大家觉得用 Rust 开发起来更快?

因为 Rust 能够让软件工程师们复用大量的 C 语言的社区资源。编程语言史发展到今天,哪门语言资源最多我想不用我在这里多费口舌,对于 Web3 这样一个日新月异的新领域来说,能够复用既有 C 语言的资源,可以轻易打败 Go 仅仅靠语言特性本身形成的便捷性优势。

简单一句话,cgo 太鸡肋,与 C 语言的兼容上,Go 也就是做到了聊胜于无而已。

这里 Web3 只是一个例子。无论进入到任何服务端之外的新领域,对 Go 来说,兼容 C 都是至关重要,没有之一。

想清楚了这一点,Go+ 面向工程领域的第一个执行目标就出来了:实现对 C 语言的完美兼容。要么让 cgo 变好,要么提供一个远超 cgo 的新的兼容 C 语言的方案。

这就是去年年底,为什么我们调整 Go+ v1.2 版本的迭代目标的原因。

目标有了,但是怎么做到呢?刚刚过去的这个春节里我几乎每天一有空就在思考这个问题。为此我查阅了大量的 github 上的代码,基本上围绕着 c2go、c2goasm、cgo、binary/asm2go、bytecode/vm2go 这些话题。

最终,我没有选择优化 cgo,而是选择了:c2go。简单说,就是把 C 代码转换为 Go 代码,然后重新用 Go 编译器进行编译。

当我把要做 c2go 这个想法发到 Go+ 贡献者群的时候,大家第一反应都是觉得不可能,这太难了。

这条路并非没有人走过。

大家都知道,Go 团队自己就做过这事(参考:https://github.com/rsc/c2go),当然他们的目标并不是做一个通用版本的 c2go,而是要实现 Go 的自举,把 Go 编译器(也包括运行时等)所有的 C 代码都转成 Go。所以它的代码可以针对 Go 编译器写特殊的转换规则,只要满足不别人肉去改编译器就好。

第三方做得最好的是 https://github.com/elliotchance/c2go 这个项目。它从 2017 年 2 月开始做,到今天有 5 个年头了,最初作者雄心勃勃,给自己定了一个小目标:在没有任何人工干预的情况下实现 sqlite3 到 Go 代码的自动转换。然而这个目标至今没有实现。

但是我认为它的选择的大方向思路是对的。这个 c2go 项目把整个转换过程分为这样几个步骤:

首先,用 C 语言的预处理程序(preprocessor)解决掉宏和各种预编译指令。这样,我们就只需要处理最纯粹的 C 代码。C 语言的 spec 非常的精简,比 Go 语言的 spec 还要短小很多,并且每个语法的功能与 Go 有足够强的相似性,这意味着这一步下来后,我们工作量相对可控许多。

这一步是重要的,它意味着平台相关的工作都在这一步干了,我们后续的步骤不用考虑跨平台。每个平台预处理结果产生的 C 代码可以不同,从而转换生成的 Go 代码也不同,这是可接受的。

其次,生成的 C 代码再通过 llvm project 中的 clang 命令行进行解析(parser):

    clang -Xclang -ast-dump=json -fsyntax-only [C源文件...]

这样就得到 C 语言的抽象语法树(C AST)。这一步自己做也可以,但是既然有现成的,能够先省心就先省心吧,以后有空了再改写不迟(注意 github.com/elliotchance/c2go 这个项目用的是 -ast-dump 开关,而不是 -ast-dump=json,这样就不得不在再写一个额外的文本解析代码来解析 clang 的输出)。

接着,就是将 C AST 转为 Go AST。这一步最为核心,我们的大部分工作都集中在这里。类似的工作我们在 Go+ 中也已经做过了,只不过我们之前做的是 Go+ AST 转为 Go AST 而已。考虑 C 语言的 spec 比 Go+ 语言 spec 要小很多,我们心里对工作量就有了大体的估计。

这一步会有少量的技术难点,比如 C 的指针是可以移动的,比如 C 语言有 union 数据类型。但总体来说这些语法都不是特别复杂,工作量是可控的。

C 是手工管理内存的,这点在转为 Go 后可以仍然保持不变。C 语言的 malloc 函数可以从 Go 进程里面划出一片内存来进行手工管理。

还有一些 corner case,比如在 C 代码里面插入汇编指令的。我们可以考虑引入一个通用的过滤机制来解决:在 c2go 转换过程的配置文件中,指定哪些函数或 C 源文件应该被忽略,然后在该工程中加入一个手工实现的 Go 源文件来自己实现这些被忽略的函数即可。

在介绍 Go+ 编译器实现原理(请移步 bilibili 搜索 "Go+ 公开课 · 第1期|Go+ v1.x 的设计与实现" 进行查看)的时候,我提到过 github.com/goplus/gox 这个项目,它是用来辅助生成 Go AST 的模块。在 C AST 转为 Go AST 中,我们也会借助它来大幅减少开发工作量。

生成了 Go AST,剩下来的工作就和我们在 Go+ 一样了,通过 Go AST 调用 go/format 这个标准库来生成 Go 源文件,最后用 Go 编译器编译它。

在我解释以上 c2go 的全过程后,小伙伴们开始相信这条路走得通。

当然,在我内心中,还有一个更为重要的理由:我们有机会干得比 cgo 更好。如果选择优化 cgo 的话,是达不到这个目标的,我们唯一能够做的只是让 cgo 更快。

如何让 Go+ 对 C 语言的支持更好?

在 Go+ 与 C 语言的互操作性上,我有以下这些核心的考量准则:

其一,C 代码不需要经过额外包装,可以直接由 Go+ 来调用。以 Hello world 这个经典程序为例,Go+ 调用 C 标准库的 printf 代码如下:

import "C"C.printf(C"Hello, world\n")

它挺像 cgo,但是它并不是。你可以拿它和cgo 的代码对比一下:

package main/*
#include <stdio.h>
#include <stdlib.h>
*/import "C"import "unsafe"func main() {
    cstr := C.CString("Hello World\n")
    defer C.free(unsafe.Pointer(cstr))
    C.printf(cstr)
}

可以看到,在 Go 语言中 import "C" 并不是真导入一个包,而只是说这里有 C 代码被插入。相比 Go 语言,Go+ 对 C 的支持更像是把 C 看做 Go+ 原生代码的一部分,而不是外来者。具体体现在:

  • 不用写任何 C 代码就可以引用。C 实现的模块如同 Go 实现的模块一样,就是正常的一个 package 而已。import "C" 表示导入 C 语言的标准库(当然不同平台不同 C 编译器的 C 标准库并不完全相同,这个考虑在 gop.mod 中对 C 库这个模块进行版本绑定)。

  • 引入了 C 的字符串常量语法 C"...",以增强 C 函数使用上的便捷性。代码中的 C"Hello, world\n" 等价于 []int8{'H', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '\n', '\0'},是一个 '\0' 为结尾的 C 字符串。

那么对于非 C 标准库,比如类似 sqlite3 这样的知名 C 库,怎么导入?我们设想是这样的:

import "C/sqlite3"sqlite3.XXX(...)

那么如果是用户自己实现的 C 库呢?嗯,最通用的形式是这样的:

import "C/github.com/foo/bar"bar.XXX(...)

很简洁,和 Go 语言写的库看起来没啥大区别,对吧?

其二,C 与 Go 的类型系统尽可能一致,以降低互操作的成本。在 C 语言中的 void (*)() 到 Go 里面就是 func(),两者属于同一个 type。其他还有 C 语言的 int 类型也是如此。不考虑引入 C.int 这类数据结构,它就等同于 Go 语言的 int。为此 Go+ 还会引入内建的 char 类型,实际上它只是 int8 的别名。

至于要不要引入 union,我估计大概率 Go+ 不支持 union 概念。但是 C 代码中定义的 union 类型,在 Go+ 中可以识别。比如 sqlite3.XXX 如果是一个 union 类型,那就会有正常的 union 行为,包括可以正确访问其中的成员变量。但是我们不允许在 Go+ 源文件中显式定义一个 union 类型。

在 C 语言中,函数会有调用约定(Call Convention)的概念。最常见的调用约定有 cdecl 和 stdcall。其中,cdecl 是 C 函数默认的调用约定,它的好处是支持类似 printf 这种允许有不定参数的函数。而 stdcall 是 Windows API 的调用约定,这个调用方式有时候会被叫做 pascal,这当然是因为它与 Pascal 语言的函数调用约定一致的原因。

这种函数的调用约定的差异在翻译成 Go 代码后会被取消。也就是说,void (cdecl *)() 和 void (stdcall *)() 这两种函数指针类型在 C 语言里面是两个完全不同的类型并且无法相互转换(强制转换后进行函数调用会导致程序崩溃),但是翻译成 Go 后就都是 func(),没有区别。

其三,翻译成 Go 代码后的程序语义,也就是 C 程序员对 C 形成的常规的语义理解都仍然正确。包括:C 字符串以 '\0' 为结尾,C 是手工管理内存的等等,这些都仍然不变。如上面已经提到过的那样,在实现上 C.malloc 会从 Go 进程中划出一片内存来进行手工管理。

最后总结就一句话:我对这个 c2go 项目相当认真。目前它已经放到 github 上 goplus 这个组织里面了,链接如下:

  • https://github.com/goplus/c2go

我觉得这事对 Go 未来十年的生态繁荣是至关重要的一项工作,否则 Go 就真的只能偏安在服务端开发领域了。

当然从 Go+ 的角度来说,它是我们在 “面向工程” 领域的目标下,给软件工程师们交出的最重要的一份答卷。


相关推荐

新手小白必知的15个超级好用的良心网站分享 建议点赞收藏

这15个鲜为人知的神仙网站我用了很久一旦上手就很难离开涵盖学习工作和生活的很多场景不仅免费还很实用随时随地学到新技能一不小心就能让你爱上它们赶紧点赞收藏吧~PPT超级市场一个完全免费的...

12款超级有用的网页交互jQuery插件

在web前端人员,利用jQuery插件可以大大加快工作效率。不是自己写不好jQuery程序,而是我写一个jQuery可能纠结几天,即使写出来,也不是最简洁,这时候就浪费了代码的质量。一般来说前端工作人...

AngularJS、ReactJS与VueJS比较 - DZone

对于一个优秀的开发人员来说,任何框架几乎没有任何实质性的区别。他们对这些技术中的任何一种都感到满意,但对于企业而言,它会产生不同。这取决于他们到底想要什么:您想开发一个对SEO友好的网站吗?如果是...

React 18 发布

3月29日,React团队宣布React18正式发布。该版本的主要更新包括开箱即用的功能改进,如自动批处理、新的AIP(如startTransition)和支持Suspense的...

软件更新丨Node.js v11.0.0 正式发布!
软件更新丨Node.js v11.0.0 正式发布!

关注开源中国OSC头条号,获取最新技术资讯Node.jsv11.0.0已正式发布!该版本主要侧重于对内部和性能方面的改进,以及升级到V87.0版本。值...

8个月前 (10-15) swdejp

Node.js v8.0.0正式发布,大量更新
Node.js v8.0.0正式发布,大量更新

Node.js基金会宣布发布Node.jsv8.0.0。Node.jsv8将是下一个长期支持版(LTS)Node.jsv8.0.0已正式发布。v8...

8个月前 (10-15) swdejp

IE 退役倒计时 3 个月,微软改进 Edge 中 IE 模式

微软曾于去年5月份宣布,InternetExplorer11桌面应用程序将于2022年6月15日退役;同时将从某些Windows10版本中淘汰InternetExplo...

Go+ 下个里程碑:超越 cgo,无缝对接 C 语言

去年(2021年)Go+的slogan从“面向数据科学”的语言升级到了“面向工程、STEM教育与数据科学”三位一体的语言。也就是说,我们希望Go+可以同时被软件工程师、中小学生、数...

乘数科技加入 PolarDB 开源数据库社区

乘数科技签署阿里巴巴开源CLA(ContributionLicenseAgreement,贡献许可协议),正式加入阿里云PolarDB开源数据库社区。PolarDB是阿里巴巴自研的新一代数据...

黑客公开勒索病毒代码 专家提醒防御查杀

新华社北京1月29日电瑞星安全专家日前截获一款病毒样本。这个被瑞星命名为“GoRansom”的病毒的作者,居然将此病毒代码在网上公开,而公开的理由,作者称是为了研究勒索病毒原理。病毒代码公开,意味着...

雷军做程序员时写的博客,太牛了。。

来源:雷军的博客blog.sina.com.cn/leijun这篇文章是在雷总个人博客看到的,里面聊到了他作为程序员的一些经历、初衷以及思考。写的不错,转来给大家看看。如果程序人生的话,这条路太漫长...

社区纠纷不断:程序员何苦为难程序员

日前,因为被多人侮辱大吼,Swift之父正式退出Swift核心团队。诸如此类的“语言暴力”、“网络暴力”事件在开源社区乃至整个IT社区屡见不鲜。多个技术社区,都出现过创始人、重要维护者、贡献...

没学历学java很难找工作吗
没学历学java很难找工作吗

说实话不是学Java很难找工作,是以后找工作基本都会看学历,学历这个东西说重要也重要,因为它决定了你的简历能不能呈现到面试官面前,如果你的学历不够的话很有可能在...

8个月前 (09-19) swdejp

Google 招聘信息泄露天机,Android Studio 可用于开发 Android 系统
Google 招聘信息泄露天机,Android Studio 可用于开发 Android 系统

Google日前在官方招聘页面放出新的招聘信息,而这个招聘信息则是泄漏了AndroidStudio的未来发展方向。根据招聘信息介绍,该职位是Andro...

8个月前 (09-18) swdejp

Docker 启用全新功能,文件共享时间大幅减少 98%
Docker 启用全新功能,文件共享时间大幅减少 98%

近日Docker更新了macOS端的桌面应用,在新版本中引入了一项名为VirtioFS的实验性功能,根据官方介绍,开启这项功能后可以使文件共享所花费...

8个月前 (09-18) swdejp

取消回复欢迎 发表评论: