分类目录归档:技术

相见恨晚的二进制序列化库:binrw

binrw 是一个针对二进制文件格式的(反)序列化库。不同于 serde 等其他的序列化库 —— serde 关注的是如何更轻松准确地把 Rust structs 映射为 serde 的内部数据模型,而序列化和反序列化这一步(即从 serde 的数据模型到文件格式的映射)需要自行编写后端,当然通用格式的后端基本都是有的,比如 JSON 和 XML。而 binrw 的针对的恰好是序列化/反序列化这一步:它能够让你更轻松地表达 Rust struct 与二进制文件格式里每个字节之间的映射关系,而无需手工编写 Serializer/Deserializer。

我一开始试图找过类似的库,也许是关键词不对、或者是看的不仔细而错过了,导致没有 binrw 可用的时候,我只能手写反序列化代码,比如:

pub fn read_pol(reader: &mut dyn Read) -> Result<PolFile, Box<dyn Error>> {
    let mut magic = [0u8; 4];

    // 读取四字节 magic number
    reader.read_exact(&mut magic)?;

    match magic {
        [0x50, 0x4f, 0x4c, 0x59] => (), // "POLY"
        _ => panic!("Not a valid pol file"),
    }

    // 序列化、反序列化时需要注意字节序
    let some_flag = reader.read_u32::<LittleEndian>()?;
    let mesh_count = reader.read_u32::<LittleEndian>()?;
    let mut geom_node_descs = vec![];

    // 读进一个数组
    for _i in 0..mesh_count {
        let unknown = reader.read_w_vec(26)?;
        geom_node_descs.push(GeomNodeDesc { unknown });
    }

    let mut unknown_count = 0;
    let mut unknown_data = vec![];
    if some_flag > 100 {
        // 只有版本大于某个值时才会有这部分数据
    }

    let mut meshes = vec![];
    for _i in 0..mesh_count {
        meshes.push(read_pol_mesh(reader)?);
    }

    Ok(...)
}

一开始对于我来说倒是问题不大,一是文件格式有限,几种格式写个遍也还好;再就是这些格式的含义都是逆向分析得出的,本身搞清楚他们的意思就很慢,写反序列化代码这部分暂时算不上瓶颈。

到了后来,问题的重要性才开始提高:

  1. 项目预计的领域扩大了。现在不光需要解析读取仙剑三的各类文件,还需要读仙剑四、五、轩辕剑、古剑等诸多游戏的各类文件,每个文件类型都手写一下,会很难受。
  2. 现在只写了反序列化的代码,序列化代码需要再写一份。
  3. 代码不够简洁。现在的代码其实还算清晰、复杂度也不高,但是 boilerplate 很多,维护起来虽不费劲但也有一些成本。文件格式增多会加重这一点。

直到解析古剑奇谭的 GameBryo NIF 格式的模型时,又去网上冲浪了一圈,果然被我找到了 binrw。有了 binrw,我就可以这样说话了:

#[binrw]
#[brw(little)]
#[derive(Debug)]
pub struct NiObjectNET {
    name: u32,
    num_extra_data: u32,

    #[br(count = num_extra_data)]
    extra_data: Vec<u32>,

    controller: u32,
}

首先是指定字节序这件事,用 [brw(little)] 就可以了。数组也不在话下,可以直接指定结构体里的成员作为数组长度。再比如:

#[binrw]
#[brw(little)]
#[derive(Debug, Serialize)]
pub struct HAnimPlugin {
    pub header: HAnimHeader,

    #[br(if(header.bone_count > 0))]
    pub unknown: Option<HAnimUnknown>,

    #[br(count = header.bone_count)]
    pub bones: Vec<HAnimBone>,
}

可选字段也可以轻松应对—— 只有在 bone_count > 0 的时候,二进制文件中才会有一段区域描述这个 unknown 结构,否则会直接进入 bones 字段的内容。再如:

#[binrw]
#[brw(little)]
#[brw(import{half_float: bool = false})]
#[derive(Debug, Serialize)]
pub struct Vec3f {
    #[br(parse_with = float_parser)]
    #[br(args(half_float))]
    pub x: f32,

    #[br(parse_with = float_parser)]
    #[br(args(half_float))]
    pub y: f32,

    #[br(parse_with = float_parser)]
    #[br(args(half_float))]
    pub z: f32,
}

一个坐标点,里面维度的数值可能是 f32,也可能是 f16。解析每个 field 时可以指定一个自定义的解析函数,同时支持传入一些参数。在这个例子中,传给 float_parser 的参数 half_float 本身是这个结构体的序列化参数,由 #[brw(import{half_float: bool = false})] 指定。使用这个 Vec3f 时,可以在外部传入这个参数:

#[binread]
#[brw(little)]
#[br(import(kf_type: u32))]
#[derive(Debug, Serialize)]
pub struct AnmKeyFrame {
    pub ts: f32,

    #[br(args{half_float: kf_type == 2})]
    pub rot: Vec4f,

    #[br(args{half_float: kf_type == 2})]
    pub pos: Vec3f,

    pub pref_frame_off: u32,

    #[br(if(kf_type == 2))]
    pub kf_offset: Option<Vec3f>,

    #[br(if(kf_type == 2))]
    pub kf_offset_scalar: Option<Vec3f>,
}

每个 binrw 了的结构体会自动生成 read、write 函数,可以直接调用进行反序列化/序列化:

NifModel::read(&mut Cursor::new(...))

现在除了一些 corner case,几乎不需要再手写任何序列化代码了。描述序列化的各种参数都跟字段本身放在一起,一眼望过去就大概知道文件格式是什么样的,序列化/反序列化时做了什么事情,查错维护很方便。

总之世界简洁了不少,绝对算得上相见恨晚。现在已经离不开了。

node-ipc:再谈开源软件与政治

这是我一直反对在自由开源软件中夹带政治立场的原因,只不过 node-ipc 的作者更进一步,将纯粹的意识宣传转变为了物理斗争。

在我之前的一篇文章里:https://dontpanic.blog/notepadpp-rust-tech-and-politics/ 也有提到,有人就离不开政治。甚至自由软件、开源软件本身也是一场政治运动[1],但这并不代表通过自由软件宣传无关的政治立场是合理的。我一般把这种行为叫做夹带私货。而这次 node-ipc 夹带的私货,有点多。

当然,开源界也有分歧、也搞政治:自由软件与开源软件之间存在思想争论[2]。因此我在开放代码时选择什么样的开源协议,其实也是一种政治。但他们只是表现为对当今资本与商业社会的接纳方式不同,与谁当总统、谁来执政没有有半点关系。

自由与开源软件所代表的精神,是人人为我、我为人人的精神,是世界大同、天下一家的精神。从道理上讲,我认为将自由与开源软件与政治立场绑定的做法,违背了自由与开源软件运动的初衷。它通过政治立场隐式地对用户社区做了限制。

那一个自由开源软件是否可以通过使用某种开源协议、或是一些附加条款,显式地撤销某些特定用户的权利呢?作为软件的作者,当然有权利自行选择协议;但需要注意的是,不论是开源软件还是自由软件 ,对协议本身是有要求的。也就是说,不是所有的协议都能叫做“开源协议”,也不是开放了源代码就可以叫开源软件。例如,开源定义[3]的第五条指出,不得歧视任何个人或群体:

5. No Discrimination Against Persons or Groups
The license must not discriminate against any person or group of persons.

再比如,GPLv3[4] 中的第七条明确指出,

……
尽管已存在本协议的其他条款,对你添加到受保护作品的材料,你可以(如果你获得该材料版权持有人的授权)以如下条款补充本协议:
a)表示不提供品质担保或有超出十五、十六条的责任。
b)要求在此材料中或在适当的法律声明中保留特定的合理法律声明或创作印记。
c)禁止误传材料的起源,或要求合理标示修改以别于原版。
d)限制以宣传为目的使用该材料的作者或授权人的名号。
e)降低约束以便赋予在商标法下使用商品名、商品标识及服务标识。
f)要求任何转发该材料(或其修改版)并对接收者提供契约性责任许诺的人,保证这种许诺不会给作者或授权人带来连带责任。
此外的非许可性附加条款都被视作第十条所说的“进一步的限制”。如果你接收到的程序或其部分,声称受本协议约束,却补充了这种进一步的限制条款,你可以去掉它们。……

我不是律师,但“某些群体不得使用”,应当不包含在上面允许的范畴中。也就是说,你可以移除它们。

因此,虽然用户社区是不受这些条款限制的,但是很显然,这种“限制某些群体融入社区”是有违开源软件和自由软件的思想和初衷的。


至于 node-ipc,就让他凉凉吧。暗暗庆幸我用的不是俄罗斯 IP。

参考

  1. Richard 对自由软件运动的看法 https://lists.gnu.org/archive/html/emacs-devel/2008-03/msg00635.html
  2. 《为什么开源错失了自由软件的重点》,Richard Stallman https://www.gnu.org/philosophy/open-source-misses-the-point.zh-cn.html
  3. The Open Source Definition https://opensource.org/osd
  4. GPL v3 非正式中文翻译 https://jxself.org/translations/gpl-3.zh.shtml

OpenPAL3 v0.3:主线剧情完结撒花🎉!

OpenPAL3 距离上次发版大概有半年的时间了,现在终于可以勉强把主线剧情推完:

景天:泥垢了

说“勉强”是在于,推进主线需要借助……内挂:

可以设置穿墙和切换地图层

以及这个:

可以调用指定脚本、切换场景、设定主线剧情进度

主要原因是一些场景机关没有实现。主线中第一个过不去的机关在蓬莱迷宫,在这之前不开内挂也没问题。梯子是可以爬的,按下互动键会瞬移上去😅

另外,OpenPAL3 现在支持手柄了!左摇杆是跑,右摇杆转动视角,B 键互动,A 或 B 键下一句对话。


这次同样可以在 GitHub Releases 和 Gitee Releases 上下载预编译好的程序:


上一个版本发布后定下的几个目标里,人物跳跃和场景机关都还没有完成。不过人物跳跃不影响主线进度,所以可以以后再做。接下来可以考虑的是:

  • 渲染:比如天空盒和光照还没做,深度缓冲也还有点问题
  • 剧情脚本:目前剧情脚本还有很多没做的指令,导致剧情演出不够润滑
  • 战斗
  • 一个酷炫的开始界面:上次的视频有人说现在的开始界面太拉跨……下次一定要做一个狂炫酷拽叼的开场!

Notepad++/Rust:技术与政治

今天偶然想起来 Notepad++ 的 Release note,又想起 rust 也有过这么一段历史:

就对裹挟自由软件夹带政治立场的行为思考了一下。我的观点是,rust core team 这么干,不妥。

rust 是个好语言,社区也不错,但没必要爱屋及乌地对 core team 的每件事都支持。users 论坛也有个帖子讨论了这件事:

帖子有点长没看完,支持和反对至少55开,甚至反对声音更多一点。

我认为不妥的首要原因是,core team 在 twitter 上公开发表政治倾向,甚至暂停了 5 周技术方面的推文更新,是一种对特权的滥用。他们在个人社交媒体上如何发言都可以,但无权代表社区发表政治言论,社区的所有人也不可能都持有统一的政治意见。

其次,rust 社区是一个欢迎所有人的社区(写在 code of conduct 第一条)。现在的潜台词就是,不支持 BLM 吗?那我不欢迎你。退一步讲,如果因为你也同样支持 BLM 所以不觉得 core team 的做法有问题,那假如将来 core team 所有人决定支持你所反对的立场呢?

有人说“有人就有江湖,技术摆脱不了政治”。本身“有人就有江湖”是一句客观正确的话,但没什么用。我做技术选型,是一种政治;社区选择使用英语(而不是世界语,例如)交流,也可以算做是政治;公司里勾心斗角搏上位,当然也是政治。但是这跟我支持民主党还是共和党有关系吗?跟我支持巴基斯坦还是印度有关系吗?先把“政治”的概念泛化扩大,说“你无法避免”,然后再具体到同属“政治”但与当前问题完全无关的点上,我们一般把这种行为叫做夹带私货。


至于 Notepad++,我认为道理是一样的。我并没有对作者的政治立场有任何意见,但很明显作为社区领导者,他对社区(包括贡献者和用户)有政治倾向的期望,客观上会限制队对立立场的贡献者和用户。而且与 Rust 不同,Notepad++ 几乎由作者一人把控。不过好在 Notepad++ 基于 GPLv3 协议开源,等有心情的时候就 Fork 一份,名字就叫 Notepad Triple Plus。Code of Conduct 第一条就写上,No politics。

OpenPAL3:仙三开源版的第二个小目标 Accomplish!

去年的时候,OpenPAL3 的第一个版本发布 之后,我给 0.2 版本设定了一个小目标:让景天能跑出永安当。当时的第一个版本还只能算是概念验证的版本,没有音乐支持、输入支持,不能直接读取仙剑三的打包文件,剧情是也在程序里面硬编码的……一年之后,景天终于跑出永安当了!✌

这次为大家提供了可运行的程序:

Gitee Releases

GitHub Releases

下载解压之后,首次运行之前记得在 openpal3.toml 文件中把《仙剑奇侠传三》的安装目录填进去。请注意反斜杠需要重复写两次哟:

之后运行openpal3.exe即可。如果运行时提示 OpenAL 出错,请下载并安装 OpenAL

目前游戏只支持键盘输入,键位为:

  • 空格键:对话框下一句
  • F键:互动
  • 方向键:跑
  • 1/2/3/4:存档至第1、2、3、4号存档位

程序还是有很多 Bug,以及有很多功能还没实现,请各位不要尝试去做奇怪的事…… 景天已经可以跑到码头那里准备上船了,但由于剧情脚本还没有 100% 支持,码头这里会用到没有实现的剧情指令而崩溃。

(以及……不要在意出现两个景天 )

另外,开发工具也全新升级!之前的模型和动画浏览器合三为一,集成在了一个工具里面:

它能够自动读取打包文件的内容,支持听歌、查看各种文件数据以及预览模型。这个工具也会读取 openpal3.toml,所以游戏路径也需要填好。


由于最近 GitHub 的访问情况不容乐观,我在 Gitee 上建了一个镜像仓库,下载也可以更快一点:

所有的开发工作还是在 GitHub 的仓库里:

以及,我们有一个新的项目主页了!还没时间仔细弄,先挂一个项目说明:


按照惯例,我们还需要确定下一个小目标。现在还没实现的功能太多了,我准备还是优先把剧情跑通,所有其他的 Bug 和系统先暂时放一下。那么第三个小目标就定为景天和雪见流浪到大渡口吧。为了实现这个目标,我们至少还需要支持:

  • 更多的剧情脚本
  • 人物跳跃事件
  • 场景机关
  • 爬梯子

希望下一个版本可以很快发布( •̀ ω •́ )y

传统的 try-catch 异常处理是否是编程语言发展中的弯路?

https://www.zhihu.com/question/425726667/answer/1525039692

我不觉得是弯路,毕竟对于多数的项目来说,写得爽比线上崩溃一两次要重要得多。

先说一下题主提到的第二点,

2) 异常处理强制了进行动态类型识别,这是一种额外的开销(虽然并不大)

题主指的应该是在异常发生时对异常对象的动态类型转换?其实这也可以看作异常处理的优势:在没有异常发生时,不需要这种额外开销。通过返回值(包括返回 Result)传递错误,由于存在更多的分支,给编译器优化、分支预测、指令 Cache 带来了负面影响;而异常通常只需要在栈上多开一点点空间(用来存放异常处理指针)。从性能上说,虽然在异常发生时它的性能要低于返回值,但在正常执行时,异常反而存在优势。

至于题主说的第一点,

许多时候面对一个子函数调用,我们可能无法知道它是否会抛出异常(当然有些语言有异常规格说明,但其实形同虚设),这导致有些时候当我们错误地假定函数不会抛出异常时,就会出现资源泄漏。写出强异常安全的代码在try-catch显得很隐晦,因而困难。

这就要说到异常的分类了。包括 C++ 和 C# 在内的很多语言,使用的都是 Unchecked Exception;而 Java 用的是 Checked Exception (除了 RuntimeException 之外)。题主谈到的,确实是 Unchecked Exception 的弊病。在我看来,Unchecked Exception 其实是最不靠谱的那种。如果一门语言使用的是 Checked Exception,那么理论上在编写强安全的代码时,难度应该与使用返回值/Result的语言没有差别。

然而,使用 Checked Exception 的 Java 被吐槽最多的,正是它的异常处理。很奇怪吧?Java设计出checked exception有必要吗?

但正如 Andrew 在一篇采访 The Trouble with Checked Exceptions 中所说,在某些时候(比如应用开发),大多数人并不关心如何处理异常,他们只关心当异常发生时我要处理好后事(C# 的 using),剩下的事就交给最上层的 catch 去做好了。这是大家喜欢 Unchecked Exception 的原因。

Unchecked Exception 很烂,但是大家就是喜欢,毕竟懒是人类的天性。

所以,一些项目会禁用 C++ 的异常,甚至不惜自己魔改一套不用异常的 STL,尤其是那些偏底层的、关键的、崩溃代价很高的项目。而大多数人做的并不是这样的项目,写得爽比线上崩溃一两次要重要得多。

题主说的第三点,

许多基于try-catch的编程语言,并不完全强制用户处理所有异常,这带来了便捷,但却又使得程序员会忽略一些本该处理的异常。

这也主要是 Unchecked Exception 的问题。对于不支持 Option/Result 的语言(比如 C 和 go),如果使用返回值的方式,其实同样会导致程序员忽略处理错误。


Rust 以及很多函数式语言的 Option/Result 确实也是我目前最喜欢的错误处理模型了。理论上,一门语言可以完全屏蔽它在底层对错误的处理方式,比如看起来是使用返回值,其实是抛了异常;或者看起来像是抛了异常,其实只是返回了一个值。因为它们只是两种不同的错误处理方式,所提供的能力是一致的;语言具体怎么表达,才是最影响用户编程体验的。我知道的唯一一个后者就是 Haskell,它让你有一种在使用传统的 try/catch 的错觉。

很多使用异常的语言对 try/catch 的支持还有其他的问题,比如语法不够简洁、处理粒度不够细等等。

另外再次推荐一下这篇文章(长文警告):Joe Duffy – The Error Model​

以及我的翻译:dontpanic:错误模型 – The Error Model​

Firefox 的 logo 上趴着的居然是一只小熊猫?

Firefox 的 logo 上趴着的不是 Fox,而是一只小熊猫!!

在成都熊猫基地拍到的小熊猫

…they opted for an animal that was not well known on the web at the time. It was the red panda. Unfortunately, people thought that the animal on the Mozilla Firefox logo was a fox. This “firefox” is actually a red panda which is a protected species in Asia. A mistake when translating red panda from Chinese to English is how we got firefox.

The Story Behind the Mozilla Firefox Logo (freelogodesign.org)

Firefox 官推大概是最大的小熊猫粉丝聚集地,官推经常发一些萌萌哒小熊猫吸粉(划掉)

不过从 Firefox 取名的历程来说(Phoenix 到 Firebird 再到 Firefox),Mozilla 一开始估计还是把它当作狐狸的,官方(或者是小编)对于它到底是狐狸还是小熊猫有时表现得很暧昧,比如这里又说它是狐狸:

不管了,反正小熊猫这么可爱,就钦定它是小熊猫了!

https://en.wikipedia.org/wiki/Red_panda

假期去成都熊猫基地,一直小熊猫刚好从我脚边溜过,简直萌到爆炸 (●’◡’●)


另外小熊猫不是小浣熊,它们长得不一样:

盗图来自下面的文章 作者翼狼Elang

认真你就输啦 (?ω?)ノ- ( ゜- ゜)つロ​www.acfun.cn

鸿蒙 2.0 模拟器里究竟有什么:鸿蒙 Java 运行时简析

这是我的两篇知乎文章 https://zhuanlan.zhihu.com/p/234397497https://zhuanlan.zhihu.com/p/243981657 的合辑。


最近因为个人问题一直又烦又颓,知乎一直没什么动态,连 OpenPal3 都几个月没更新了。但是从昨晚开始好几个小号想过来打我的脸,原因是我一年之前回答了这个问题:为什么很多人不相信鸿蒙系统是真的?​

姑且不说你们搞清楚我讽刺的点在哪没有,请问你们可以拿现在的剑去圆去年前年吹的 b 吗?作为一个程序员,始终对华为终端在软件上的吹逼行为十分讨厌。而且花粉们似乎已经接受了这样的现实,还觉得“大嘴吹过的皮全都实现了”。这种空手套沸腾的行为,不是华为还真不敢干。论营销,你以为华为在负一层,其实他在第五层。这些人搞得我心态爆炸真的烦,我就想看看鸿蒙 2.0 到底是个什么东西。

一开始大家说华为电视上的鸿蒙 1.0 是安卓套壳,很多人纷纷跳出来说“adb 说明不了问题”,颇有见到胳膊就想到大腿的意味;后来又改了话术,说“按照路线图,1.0 就是安卓套壳。怎么了?”。那按照路线图,请问 2.0 就应该不是安卓套壳吧?

DevEco Studio

所以我去下了 DevEco Studio。需要注意的是,DevEco Studio 并不是新做的,华为之前就有这个东西,是开发安卓的:文档中心​

DevEco 1.0 是用来开发安卓的

我一开始就找错了,这个是1.0。2.0 把安卓的支持“删掉”了,换成了鸿蒙。当然这都没什么问题,用 IDEA 二次开发也没什么问题,只是预防海军把 DevEco 拿来作为鸿蒙早就存在的论据。

工程方面现在支持新建电视、智能手表和半智能手表(个人归类,不喜你对)。电视和智能手表支持 Java 和 Javascript,半智能手表只支持 Javascript。比较关心电视的 Java 实现,所以新建了这个。

package com.example.myapplication;

import com.example.myapplication.slice.MainAbilitySlice;
import ohos.aafwk.ability.Ability;
import ohos.aafwk.content.Intent;

public class MainAbility extends Ability {
    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        super.setMainRoute(MainAbilitySlice.class.getName());
    }
}

Emmmm…. 虽然我不做安卓,但是很久以前学过一点。这好像跟 Activity 有点像?不过也没关系,毕竟(做)好的 API 设计拿来用用,也不是不行。

下面我就想去看看 Ability 的代码。SDK 里面包含的全是 Stub,我去 OpenHarmony 上也没找到 aafwk 的源代码。只找到了 aafwk-lite,但这个明显不是电视上用的版本。各位海军请不要再随便丢一个链接过来说“鸿蒙已经开源了链接在这你还不服吗”,你们自己有去看过吗?还烦请哪位大佬找到了 aafwk 的源码的话留个言。

模拟器运行

找不到就算了,DevEco 还有一个 HVD 管理器,我想这既然模拟器都有,里面运行时肯定是全的吧?

不过这个 Manager 还需要下一些包才能运行,下载一直磕磕绊绊的出问题。这也导致我现在才把模拟器抱起来。

运行模拟器需要登录华为账号?也罢毕竟我以前用过荣耀手机,华为账号还是有的。然后实名注册了一波回来发现,模拟器运行每次限时 1 小时?大概各位也能猜到了,这模拟器其实是跑在服务器上的,界面再串流串回来!

这就堵住了我想看看模拟器里面内容的想法。也行,我先把 Hello World 跑起来吧。跑了 HelloWorld 起来之后,很快就会有 log 打出来。这个也有别人发过了,各位估计也见过:

嗯?

如果你随便下个断点,还能看到更具体的:

嗯?×2

点开 .shadow class,还可以看到:

可能有点看不清,我贴过来:

嗯?×3

dalvik 出现。以及,编译生成的 hap 是个 zip 吗?

嗯?×4

既有 dex 又有 apk,我们把 apk 再解一下,看看究竟是不是大白腿:

嗯?×5

半智能手表

上面也提到过,半智能手表只能使用 Javascript 开发,这是与电视和智能手表不同的地方。其实它们还有另一个不同之处,就是电视和智能手表支持由在服务器上串流调试,但是半智能手表不行。而且它连本地的模拟器(Emulator)都没有,只有一个用 Node 做的 Simulator:

启动 Simulator 的防火墙弹窗
本地 Node.js 做的的 Simulator

华为目前开源出来的代码,都是 Lite 版本,是基于它 2016 年就开源了的 LiteOS 做的。所以有理由相信,现在只有半智能手表会用 LiteOS 版本的鸿蒙。而且现在连完整的模拟器都没有,只能用 nodejs 顶一下。

结语

所以我觉得到这里可以大胆地做出结论了:

电视和智能手表上,完全就是安卓套壳;只有半智能手表,是华为在它 16 年开源出来的 LiteOS 的基础上做出来的。

以华为在嵌入式领域的积累,花一年多做成 LiteOS + 半智能手表这样的进度是合理的。这个答案 如何看待 9 月 10 日华为发布的鸿蒙 OS 2.0 系统,应用前景如何?

里有一段话,我想摘录在这里:

华为本来有两个选择:
1. 我们被美国制裁了,我们别无他法,我们决心从现在开始,用3到5年的时间,自主研发我们的一套体系,摆脱对美依赖;
2. 我们被美国制裁了,但是我们早就做好了准备,已经掏空安卓,随时能拿出鸿蒙来替代,引起一片沸腾;然后顶着沸腾、质疑和嘲讽暗戳戳的加班加点3到5年,终于拿出东西来。
我始终搞不懂的是,为什么华为选择了方案2,因为华为高管喜欢沸腾?
两种选择反映的其实是一家公司的做事态度,反映的是它的诚信度和责任感。

作者:世界树的影子
链接:https://www.zhihu.com/question/420404904/answer/1465210355
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

说实话,要是我的老板把牛逼吹出去了(难听点说就是造假),但我现在的代码做不到,我会脸上滚烫,生怕谁过来问我说“这个 Feature 是还有问题吗?这里是还有 Bug 吗?”。不知道华为的程序员会不会如此。如果也是这样,我能做的也只是摸摸他们受伤的心灵了。

海军还要如何吹?

我也想了一下,海军还能怎么吹呢?

  • 不管,就是打你的脸了!鸿蒙真的存在!
  • 看到 apk 就想到安卓?鸿蒙兼容安卓,这不是很正常吗?
  • 你知道做操作系统多难吗?不用一些开源的东西,你能做出来?
  • 按照路线图,鸿蒙 2.0 就是这个样子,3.0 明年见。
  • 我只能说鸿蒙是中国的希望

当然,具体鸿蒙是什么里子他们是不会管的,华为是不是吹牛逼他们也不会管的。要是华为不吹了,他们大概反而会失落吧。


距离 9 月 10 号已经过去很多天了,鸿蒙的开水终于逐渐平息。我在 上一篇文章 (饱含了对沸腾人民、沸腾现象的愤怒情绪)中最后的总结是:

电视和智能手表上,完全就是安卓套壳;只有半智能手表,是华为在它 16 年开源出来的 LiteOS 的基础上做出来的。

如果你还对现在电视上的鸿蒙心存幻想,觉得鸿蒙不是安卓、觉得鸿蒙是在兼容安卓、觉得安卓是鸿蒙的子系统,这篇文章会更加清楚明白地告诉你真相;如果你认可了鸿蒙是安卓套壳,那么这篇文章会从技术角度探究一下,这层壳到底有多厚。

HDC:鸿蒙版的 ADB

前两天刷 IT 之家,刚好刷到这篇帖子:[系统] 鸿蒙2.0:API/SDK逆向-模拟器鉴赏-LiteOS对比

原来用鸿蒙版的 adb,是可以直接连上 Shell 的… root 之后读取 build.prop,证实了系统就是安卓 10。随便看看,还有大量的安卓包、动态库、目录结构什么的,也能够佐证。

既然能登上 Shell,那也就可以把运行时拉下来,看看鸿蒙的运行时到底是什么样子的。

boot-zframework.z.vdex

直接

grep -Rl ohos /system

看下来一圈,猜测 /system/framework/boot-zframework.z.vdex 就是鸿蒙的 Java 运行时,pull 下来一看确实是。vdex 叫做 pre-validated DEX,是安卓新引进的格式。可以用下面的工具anestisb/vdexExtractor​

把 vdex 转换回普通的 dex(转换完会有两个 dex)。直接扔进 jadx 就可以反编译了:

很多在 SDK 里面不提供的类,都在这里面

如果跟 SDK 对比一下,会发现很多 SDK 中不存在的类使用了安卓的包。从设计上,大多是使用了 Delegate/Proxy/Adapter 避免了上层代码直接依赖安卓,这也是屏蔽实现细节的常用手段。不难看出,虽然华为在 SDK 剔除掉了安卓的依赖,但现在鸿蒙仍然是在安卓之上做的封装。

鸿蒙的分布式特性

在继续分析这层壳的薄厚之前,我觉得有必要先说一下鸿蒙的分布式特性。看了一圈代码下来,是能够感觉到鸿蒙有自己的设计和想法的。这一段完全来自我对反编译代码的理解,未必完全正确,也代表不了鸿蒙真正的思路。

基本想法是:把 API 做成 RPC Call。

鸿蒙的 Ability,比安卓的 Activity 概念要宽泛一些,更像是一个 Service。比如 Wifi Device 是一个 Ability,Wifi Enhancer 是一个 Ability,Wifi HotSpot 也是一个 Ability,另外还会有一个 Registra 来做服务注册和查询。具体这些 Ability 是由谁提供的不是很重要,毕竟都是 RPC Call,理论上不在本机也可以。在 SDK 这一侧,这些功能会被包装成普通的 API。

当然这些都不是新概念,老旧的 COM 就已经可以统一 RPC Call 和本地 Call、也连带提供服务注册和查询。后端上这种微服务架构也比较常见。不过,把操作系统的 API 也做成这种结构,我确实不清楚现在有其他的系统在做。

鸿蒙的壳,有厚有薄

运行时里面包含了很多的组件,完成度各不相同。我把它们大致归为四类,从上到下完成度递减:

  1. 已经按照上面的设计,做完了 RPC 封装、在 Java SDK 和 Service 一侧都不再依赖安卓组件
  2. Java SDK 一侧已经不再依赖安卓,用 JNI 调用了自己封装的 native library,但是在 native 一侧仍然需要依赖安卓
  3. Java SDK 一侧还是需要依赖安卓组件,但是也有依赖自己封装的 native library
  4. 安卓 API 映射大法

我没有全部看完所有组件。目前看的组件里,只有部分相对较轻、比较独立包能够做到第一类(比如 wifi 和 bluetooth),大多组件都是第二和第三类(包括 Ability/AbilityShell/Agp/Usb等等)。第四类也有,典型的比如 os.ProcessManager。

先举一个第一类的例子吧,比如 WifiEnhancer:

这是个单例,在新建对象的时候会把需要的 AbilityId 传进去存起来。API 都是 Message Passing:

在每个函数最后的 request 里,会使用 AbilityId 去 Registra 里面找相应的 Ability 缓存起来,然后直接对他 Rpc call。

这段代码的反编译有点问题

再举个第二/第三类的例子吧:Agp 是华为自己的自绘组件库,封装了常见的 UI 组件和自绘能力:

components 里是 UI 控件,render 里面是绘图 API

components 和 render 里面都是对自己 native library 的 wrapper。在 SDK 一侧,还是需要有 Adapter 来依赖安卓包;而在 native library 一侧,也还有对安卓的依赖:

libagp.z.so 的导入表

上面是 libagp.z.so 的导入表,对安卓的依赖不多。它还导入了很多 Skia 的方法,基本可以是确定是用 Skia 自绘。Skia 是谷歌开发的图形库,不过是跨平台的,可以不算安卓独有。

下面是 libagp.so 的导入表,还依然有大量的安卓导入函数:

libagp.so 的导入表

其实根据国内每家都有自己的安卓 UI 来看,各厂应该都有类似的库,只是未必直接作为 Java API 提供给 App 使用。所以我就好奇去下了个 DevEco 1.0,华为在 1.0 里面提供了线上的手机模拟器。开了个 P40 Pro (Emui 10),里面也有 libagp。(所以盲猜这个 agp 本来是 Emui 里面的自绘库,在鸿蒙里面做了一些新改动,暴露给了 App。)

(此处应有截图,但是华为的线上模拟器经常点不了允许调试…)

再放一个第四类的吧。不过 ProcessManager 本身足够简单,不需要过度封装:

请原谅我看到这的时候真的笑了…我不是故意的

结语

按照目前的分析,我认为鸿蒙如果真的想完全不依赖安卓,还有很长的路要走,个人乐观估计往后一到两年都会是安卓套壳/基于安卓/安卓魔改的状态。跟大嘴的 PPT 更是差得远,更谈不上已经掏空、随时替代、比安卓快 60%。而且似乎并没有要走 WSL1 的路线,这样改下去,要是未来某一天真的把安卓所有的东西都换掉了,肯定是没办法二进制兼容安卓 App 的,连源代码级别的兼容都做不到。

最后,还是想带一点私货。这几天大概看到了热心群众这样的心路历程:从“鸿蒙横空出世”,到“鸿蒙囊括了安卓”,到“鸿蒙在兼容安卓”,再到“华为是在诈美国”,最后是“你说的都对,但是华为需要群众的沸腾来支持”… 难道现在天道有轮回,再次进入亩产三万斤的时代了吗?

若真是如此,那美帝看我们,跟我们看三哥,恐怕没什么大区别。

OpenPAL3:做一个开源版本的仙剑三

《仙剑奇侠传三》是我小时候最喜欢的游戏之一了(另一个是《新剑侠情缘》),沉迷神鬼妖怪世界无法自拔,只有发烧卧床不起才能阻挡我玩游戏的热情。ummm 当时发烧算是原因之一吧,另一个重要原因是玩到锁妖塔四层过不去了_(:з」∠)_

前一阵大概是因为偶然刷到 OpenDiablo2 让我突然想到,为什么不来做一个 OpenPAL3 呢?让自己喜欢的游戏重新活一次,也是很有意思的一件事。如果顺利的话,说不定还能扩展一点私货,比如做个战棋战斗模式?不过这都是后话了,得先把基本的原始游戏功能做出来才行。

所以我就踏上了收集当前前沿科技进展的旅途。首先发现的是 @zhangboyang/PAL3patch 这个仓库,虽然名字叫仙剑三分辨率补丁,其实还包含了相当多的其他功能。最关键的是,这里提供了一个便捷的脱壳工具,为我们分析程序扫除了障碍,可行度++。

另外一个值得一提地方是仙剑三高难度贴吧。贴吧不愧是互联网中文社区的扛把子,这个高难度吧里面聚集了很多做仙剑三 mod 的神犇,全都是人肉编辑二进制的资源文件,拷来改去还能不出错。贴吧里有一些分析目录结构的帖子,是入门上手的好地方。还有一些关于游戏机制和数值的分析,但对文件格式的解析比较少见。

另外还搜罗到几篇零散的文章,其中一个是看雪论坛的一篇帖子,分析了仙剑三剧情文件的格式以及压缩包的压缩格式。另一个是一个转贴,原博客已经年久失修找不到了,这个帖子简单分析了仙剑三的一种模型文件的格式,不过尚不全面。最后一个是开源的仙剑三解包工具,实际试用一下,还有一些 Bug,解压出来的文件可能会有不正确的地方。

最后的最后,是藏在仙剑三游戏资源里面的意外惊喜。解包出来的文件当中,有个别文件是游戏开发过程中误打包进来的,比如剧情脚本的源代码(一种非常简单的类似于汇编的语言),还有少许说明性的文本文件,不过提供的信息并不是很多。

这些就是目前巨人肩膀的高度了,剩下的就得靠自己了。首先是先定一个小目标,否则步子太大容易扯到蛋:目标就定为能够播放游戏开头景天跟雪见登场的那一小段剧情好了。

为了实现这个目标,我们得需要:

  • 本着自娱自乐的原则,引擎肯定是要自撸的,所以至少得有一个能凑活用的引擎
  • 模型文件格式的解析,能加载模型是最基本的要求了
  • 场景文件格式的解析,得能把景天的小屋子搭出来
  • 剧情脚本文件可以暂时不用完全读出来,毕竟那一小段剧情不长,把剧情脚本硬编码一下也是可以的

大概就需要这么多的工作吧,这样就可以实现第一个小目标了。从过年前后开始开工算起,到现在大概做了两个半月了,如果从撸引擎开始算要更早一点——现在我已经完成第一个小目标啦!虽然这篇文章发在四月一号,但这不是愚人节忽悠,不是愚人节忽悠,不是愚人节忽悠!

https://zhuanlan.zhihu.com/p/122532099

各位放心,你们的耳机没有坏,是我还没有加声音;文本框也是临时用 Imgui 默认的框框对付了一下,立绘表情也还没贴,场景的环境光也还没有设置(本来应该是烛光烁烁的夜晚😅)……目前还只是 Demo 的程度,非常多的硬编码,相当多的技术债,还有很多文件格式上的问题没有弄清楚。不过不管怎么说,小目标算是勉强达成了,就把它叫做 v0.1 吧。

在达成小目标期间,也是要做一点辅助工具的,比如模型浏览器:

永安当场景静态模型
景天 – 人物顶点动画

场景浏览器:

渝州西南

那么接下来的第二个小目标的话,就定为能够跑出永安当吧。要达成第二个小目标,需要解决的问题非常多,输入控制、声音、碰撞、事件触发机制还都没做,大概没有几个月搞不定,道阻且长 _(:з」∠)_

仓库在此,欢迎 Star 🧐 https://github.com/dontpanic92/OpenPAL3​

Rust 面向对象 FFI

说到 FFI,FFI to C 大概算得上是事实标准了。Rust 也提供了 to C 的 FFI,但如果我们想把 Rust 结构包装成对象提供给其他语言使用,事情就没那么简单了。


一种方法是完全依赖于源语言的对象模型,典型的例子是 SWIG。SWIG 是一个 C/C++ 的 Wrapper Generator,通过 parse C/C++ 头文件、结合不同的后端,可以输出不同语言的绑定。它的思路是比较简单的:C++ 中的成员方法全部包装为 extern C 的普通函数,接收 C++ 对象的指针;目标语言这边的对象只携带一个 C/C++ 对象的指针就可以了。我之前用 SWIG 做过一个 Wrapper,wrap 了 wxWidgets(一个 C++ 库)给 golang 用,比较头疼的是:

  • 内存管理。一般的 C++ 类都不会自带引用计数,这时候怎么处理裸指针就很头疼,每个函数的返回值和参数都需要(人工)分析出对象的所有权是否转移,以便决定需不需要在 Go 这一侧调用 delete。同时,如果所有权在 C++ 一侧,如何判断对象是否已经释放,仍然比较难办。如果 C++ 一侧的库完全使用智能指针还好一些,但这件事情高度自由,不可强求。
  • ABI 问题。上面提到了这种方法依赖于源语言的对象模型;但不仅如此,我们还依赖于编译器所使用的 ABI。例如,如果库是 gcc 编译的,wrapper 也需要是 gcc;甚至 gcc 的 Library ABI 的变化,也会使我们受到影响。
  • SWIG 的前端后端都是在一起的,中间表示就是 AST,除了直接加代码也无法自行扩展。如果要做 wrapper,最好是能够自己控制 wrapper generator。比如,Go 语言不支持重载,那 C++ 的重载怎么办?如果我们想要的与 SWIG 的不同、或是有自己特殊的情况需要处理,就需要自己 fork 了——这时候,所有与后端不相干、但又不得不知道的东西,就会成为负担。
  • (改 SWIG 的那个 Go 后端真的很难受

目前 Rust 似乎还没有 SWIG 这样工具,只找到一个 cbindgen,可以生成 C/C++ 头文件。C++ 头文件的话,比较有特色的是能够支持模板,但 impl 之类的还是无法放进 class 里面去。


另一种方法是,在源语言一侧,先手工包装并导出为一个标准的、跨语言的对象模型。谈到跨语言的对象模型,跟我有过交集的也就两个:一个是 COM,一个是 GObject。

先聊一下 GObject 吧,由于用过 GtkSharp,对 GObject 多少有一些了解。Rust 的 Gtk 绑定 gtk-rs 提供了方便手段,可以让我们在 Rust 里面 subclass GObject 类型,但这还是不满足我们的需求,因为它并不能支持在 Rust 中定义并导出新的类型给其他语言使用。

能够实现这个目的的,是一个叫做 gnome-class 的库。(作者居然是 Gnome 的 Co-Founder 😮)它能够生成 GObject 规则的 C 语言导出函数,同时可以生成 GIR 文件(GObject Introspection Repository)。

GIR 描述了 GObject 的类型,很多动态类型语言都有 gobject-introspection 库的支持,它能够直接读取经过编译后的 GIR(叫做 typelib)。也就是说,只要有了 GIR,理论上不再需要任何的其他步骤就可以在 Python/Ruby/Lua/Javascript 里面使用了我们的库了。

对于静态类型的语言,还是需要有代码生成的步骤的。不过一般来说,只要这门语言有 Gtk3 的绑定,多半都会有代码生成器的,比如 Rust/Haskell/D 之类的语言。

我实际试了一下这个库,目前还远远谈不上可用,但在几经 hack 之后终于能够正常在 Python 里面 Subclass 一个 Rust 的 struct 了:

Rust 一侧,我们需要把 Wrapper 写在一个 macro 里。class/virtual 什么的语法稍稍显得有点奇怪;还有一点点我 hack 的痕迹:

gobject_gen! {
    #[generate("generated/application.rs")]
    #[generate("generated/application.gir")]
    class Application: GObject {
        application: GiApplication,
    }

    impl Application {
        pub fn initialize(&self) {
            self.get_priv().application.0.borrow_mut().callbacks_mut().g_object 
                = <Self as glib::translate::ToGlibPtr<'_, *mut ApplicationFfi,>>::to_glib_none(self).0;
            self.get_priv().application.0.borrow_mut().initialize();
        }

        virtual fn on_updated(&self, _delta_time: i32) {
            println!("in application on_updated");
        }

        pub fn run(&self) {
            self.get_priv().application.0.borrow_mut().run();
        }
    }
}

生成了 GIR 之后,使用 g-ir-compiler 把它编译为 typelib,就可以直接在 Python 里面导入了:

from gi.repository import Application

class App(Application.Application):
    def __init__(self):
        super().__init__()
   
    def do_on_updated(self, delta_time):
        print("delta_time " + str(delta_time))

a = App()
a.initialize()
a.run()

至于 COM,最常见的就是 DirectX 的 API 了。可能大家觉得它是 Windows-Only 的技术,但其实 COM 作为 Object Model 来说仍然是跨平台的。微软自己有一个 com-rs,还有一个支持跨平台的 intercom。大概是因为 COM 全部都是接口导出,要比 GObject 简单一点,所以只需要在 Rust struct 上加一个 Attribute Macro 就可以了。这是一个 intercom 的例子,相比于上面的 gnome-class 简直轻松了一个档次:

pub use intercom::*;

#[com_library(Calculator)]

#[com_class(Calculator)]
struct Calculator {
    value: i32
}

#[com_interface]
impl Calculator {
    pub fn new() -> Calculator { Calculator { value: 0 } }

    pb fn add(&mut self, value: i32) -> ComResult<i32> {
        self.value += value;
        Ok(self.value)
    }
}

还是有不少语言支持 COM 的,至少 .net 上的语言都会比较容易;其他的比如 D 也支持直接与 COM 组件交互。


一大圈下来,似乎导出为 COM 用起来最方便,只是在目标语言一侧支持力度没有 GObject 那么大,也不知道实际用起来有没有坑。FFI 终究是一件很繁琐的事啊😥

当 Neovim 遇上 Acrylic

dotnvim 是很久之前开的坑了,当时是想做一个颜值在线的 Windows neovim 前端。但其实我很少单独使用 vim,用也多半是在别的 IDE 里面用 vim 插件,所以这个项目也没怎么更新过;而且新的 Windows Terminal 完善之后,大概 dotnvim 存在的意义又下降了一分。但今天又打开看了一下,发现颜值其实还是挺不错的,居然又燃起了我间歇性使用 vim 的欲望⚠顺便再写一遍文章回忆一下当时开发时遇到的坑🤔

没有可用的 C# neovim 客户端

虽然当时在 neovim Wiki 的 Related Projects 里面有列出一个 C# 客户端,但实际看过就会发现功能欠缺很多,几乎处于不可用的状态。所以只好自己再造一个轮子了。neovim 与前端的通信使用的是 message pack,是一种很容易理解的协议。整个客户端的实现也很简单,就是 neovim 说什么,我们照做就行了——比如光标移动到哪里、颜色设置成什么、显示什么字符之类的。

但是只看 neovim 的 文档 还是会不明所以,所以搞不懂的时候就只能去翻官方的 Python 版或者是 Qt 版的客户端代码了。

Acrylic Blur

一开始我是打算做成 UWP 的,毕竟亚克力效果只有 UWP 才支持。起初考虑 nvim 需要自己单独一个进程,担心 UWP 不支持,结果是我多虑了。但后来翻了一下 UWP 的权限问题,似乎保存文件到任意位置的唯一方法只有弹出一个文件选择框,这与 vim 用户们的习惯大相径庭。最后就还是决定做成一个 Win32 的程序。

这样一来,如何实现 Acrylic Blur 就是个问题了。不过好在当时已经有很多关于 Windows 未公开的 API SetWindowCompositionAttribute 的资料了,所以自己琢磨一下试试也不难。(画外音:Windows 的兼容性负担就是被你们这群人搞上去的😀

当时还有很多采用自绘的方案,但跟原生的亚克力效果还是无法做到一致的。至于如何让整个窗体全都被透明特效覆盖到,这就属于 DwmSetWindowAttribute 和 DwmExtendFrameIntoClientArea 的工作了,这两个 API Aero 的时候就有了,幸亏以前用过,上手速度++。

Winform 还是 WPF?

本来准备使用 WPF,还在 WPF 和 D3D 交互上搞了半天,用了这个看起来没人维护了的 WPFDXInterop,它提供了一个 D3DImage 控件,只要把渲染好的图贴上去就可以了。但是最关键的是用它搞不定透明,如果在 WPF 里通过 WinformHost 的嵌入一个 Win32 控件的话,透明效果又不对。

所以最后我选择直接就使用一个 Win32 的窗体,上面所有的内容全部自己用 D2D 画,反正主体界面也不复杂,顶多就是几个 Button、再加上一个简单的 BoxLayout。至于其他的对话框(比如设置),就直接使用 WPF 就可以了。

1px 的不和谐边框

由于 Windows 10 上窗口周围都有 1 px(设备无关像素) 的边框,当我们的亚克力背景透明度调的比较低时,这个 1px 的边框就会露出来。这个问题实在无解,最后我就暴力地用 vim 当前的背景色不透明地先把这 1px 画上,盖住系统的边框,至少颜色上与当前使用的 Color Scheme 能够统一。如果我们的背景没那么透明,这样的边框就不显眼了。

就是这个黄色边框,其实是为了盖住系统的边框

次像素抗锯齿

这是我一直都没有搞定的一个问题。由于我们窗口的背景是透明的,次像素抗锯齿没办法启用,只能使用灰度抗锯齿。但灰度抗锯齿在低分辨率屏幕上的效果并不是很好。

我还特意去看了一下 UWP 的抗锯齿效果,也不是次像素抗锯齿(放大之后没有明显的红绿蓝边界),但好像要比普通的灰度抗锯齿的效果要顺眼(难道是错觉?)。好在我用 Surface,灰度抗锯齿效果在高分辨率屏幕上还不错,眼不见心不烦😞

连字 Ligature

怎么在 D2D/DirectWrite 上启用连字也花了我不少的时间。连字就是现在很多人喜欢的那种把多个字符(比如->、==)画在一起的那个功能。

要实现连字,首先需要通过 TextAnalyzer 分析输入的文本,得到一组 Glyph 的索引,以及一组 Cluster Map。Cluster Map 里面村的就是 Code Point 到 Glyph 直接的对应关系。比如,如果 Cluster Map 的内容是 [0, 1, 1, 2, 4],那就代表第 1、2 个 Code Point 对应了一个 Glyph(比如 Ligature 的情况),而第 3 个 Code Point 对应了 2 个 Glyph (比如阿拉伯文上面的各种修饰)。

 Codepoint Index    Glyph Index
       0  -----------   0
       1  -----------   1
       2  ----------/
       3  -----------   2
          \----------   3
       4  -----------   4

最后使用 DrawGlyphRun,把几个 Glyph 一起画出来。如果字体提供了连字支持,那么在 Cluster Map 中就会有所体现。

C++/CLI 做胶水

有时需要跟 Native 的 Win32 API 做交互,这个时候就会感叹有一个 C++/CLI 这样的语言做胶水实在是太方便了。但 C++/CLI 的 Bug 也不少,有时会遇到报编译器内部错误,建议我把 XX 行附近的代码换个写法🤣 现在在 dotnet core 的趋势下,大概 C++/CLI 历史任务也快圆满完成了,可是替任者在哪呢?

关于这个项目的一些回忆大概就这么多了。最后还是要感慨一下,跟调原生 DirectX API 相比, 用 SharpDX 确实舒服很多,这可能跟 C# 的语法糖们也有关系8️⃣。

Redo Build System

前一阵子想找一个简单的 build system,看到了 redo,就去研究了一下。一开始还有点迷糊,但最后发现,虽然 redo 看起来只做了一件事(当文件变化时重新执行脚本),但它其实还解决了另一件更重要的事:如何在一个命令式语言中描述结构信息?

声明式语言是擅长描述结构信息的,谁是谁的父亲、谁是谁的兄弟、谁依赖了谁、谁的属性是什么等等。Makefile 就是一种典型的声明式语言,大多数 build system 也都使用声明式语言来描述构建流程(也有的使用了同时掺杂了命令式和声明式的语言,比如 Meson)。

回到一开始的问题,如何在命令式语言中方便地描述这种结构信息?熟悉函数式编程的人大概一下子就会想到使用 Method Chaining:

target("all")
    .depends_on("other_target")
    .source_files("a.c", "b.c")
    .call("...")
    ....;

虽然很多语言都能模仿出这种写法(你甚至可以用 bash 写 fp),但这已经不算是典型的命令式语言的用法了。同时,它仍然是“提前声明”式的做法(即在 build target 之前声明好这个 target 的所有属性、依赖等等),不够灵性。Redo 的做法同时也解决了 Makefile 的一些其他问题,例如依赖的其他 Make 文件是在编译过程中产生的情况、或是 Recursive Make Considered Harmful

Redo 的做法其实很简单,就是让脚本一边跑,一边收集脚本或文件之间的依赖关系。这是它与其他声明式语法的 build system 的最大不同之处。这样的好处是,build 过程仍然用 bash 描述(比起用 Method Chaining 自然很多),而且依赖关系可以随时添加——甚至在编译结束之后。在命令式语言里看起来这是再正常不过的事了:

# 依赖 $2.c
redo-ifchange $2.c
gcc -MD -MF $2.d -c -o $3 $2.c
read DEPS <$2.d

# 依赖 gcc 生成的头文件列表中的所有文件
redo-ifchange ${DEPS#*:}

稍微跑个题,我很久之前其实也想过类似的问题:如何在 C++ 中描述一个图形界面?这个问题与虽然跟 Redo 半毛钱关系没有,但解决的思路其实完全一样。最后我做到了这样:

这是完全合法的 C++ 代码,扔进编译器就能编过。它描述了 main_form、button1 和 button2 之间的结构关系(父子关系),看起来像是声明式的语法,但仍然不丧失命令式语言的特点。最后只要:

int main()  
{
    yz::app app;
    yz::window* w = yz::ui["main_form"];
    app.exec();
    return 0;
}

就能启动。实现细节请看 https://zhuanlan.zhihu.com/p/24196189


至于如何看待 Redo,我觉得它小众自然有它的原因。一个 build 脚本,目标的依赖关系方面使用声明式语言更容易表达,而每个目标的产出过程是一个命令式的过程。所以路无非三条:可以在声明式的语法中加上点命令元素,或是在命令式的语法中加上点声明元素,或是干脆搞一个半声明半命令式的语言。Make 走的第一条路,Redo 是第二条,Meson 是第三条。

从结果来看,大家明显认为解决目标之间的依赖问题更重要。