作者归档:dontpanic

传统的 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​

0

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

0

鸿蒙 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 的,连源代码级别的兼容都做不到。

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

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

0

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

场景浏览器:

渝州西南

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

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

0

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 终究是一件很繁琐的事啊😥

0

当 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️⃣。

0

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 是第三条。

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

0