Emake:你见过最简单的 C/C++ 构建工具

CMake 已经成为 C++ 构建工具事实上的标准了,即便觉得它很难用,但项目发布,跨部门协同,基本都以 cmake 为准。尽管你可能觉得其它构建工具更顺手,没问题,你们平时用就行,但项目发布或者跨团队协同时,你得同时用上 cmake 来标准化。

那么对于内部中小项目,非正式个人练手项目,或者非发布阶段的开发过程,是否也需要上 cmake 呢?还真不一定,一旦不用 cover 整个宇宙的构建需求,我们大可以找一个趁手的二号构建工具,满足平时使用。那么哪个二号构建工具值得推荐呢?

很多流行的构建工具,从 xmake 到 meson,恐怕都不适合,因为他们都试图同 cmake 去竞争试图要 cover 整个宇宙,即便号称精简,也不可能精简到哪里,尽管他们最简单的 demo 看起来好像真的超简单,但再稍微复杂点,比如考虑多平台架构,加个 release/debug 和包管理,一个个都变得丑陋不堪,立马原型毕露,因为他们都是命令式的。

我从 2009 年开发了一个叫做 emake 的构建工具,就是一个 emake.py 的单一脚本,持续使用并陆陆续续迭代了 15 年,今天感觉可以让他出来走两步。

推荐它,因为它有可能是你见过最简单的构建工具了,简单到什么程度呢?

(点击 more 展开阅读)

Continue reading

Loading

Posted in 未分类 | Tagged | 2 Comments

CD3:Flash 开发宝典

前段时间碰到个经典的 Flash 游戏想玩一下,发现原网站挂了而游戏又需要验证原网站,于是想对其稍加修改,才发现原来可以下载 Flash 相关开发工具的页面已经全停了:

所有 Adobe 官网可以下载 flash 插件,播放器,SDK,Flash Builder 之类的地方,全被替换成了上面的页面,也就是说今天 2024 年你已经无法从官方渠道再获得一套完整的 Flash 开发环境了,而其他网站但凡提及这些资源的,都是指向了官方地址,也都会被重定向到上面的内容。

于是我想,趁着现在部分网络资源还未失效,以及我老电脑里还有一些资料,是时候对整套 Flash 开发环境进行一次整理和快照了,避免将来有一天想编译一下老项目出现尴尬。

本光碟包含 Flash 全胜时期的完整开发环境,包含 Flex 各版本 SDK,AIR 运行时和 SDK,各版本播放器,相关工具,以及经典轻量级 IDE – FlashDevelop:

虽然 Flash 官配 IDE 是 Flash Builder,但懂行的都知道,那玩意儿臃肿庞大不说,项目稍微一大点,就会卡到没法用,所以真的动手,大都会使用更加小巧流畅的 FlashDevelop。

(点击 more/continue 继续)

Continue reading

Loading

Posted in 未分类 | Tagged | 1 Comment

56 行代码用 Python 实现一个 Flex/Lex

作为 Yacc/Bison 的好搭档 Lex/Flex 是一个很方便的工具,可以通过写几行规则就能生成一个新的词法分析器,大到给你的 parser 提供 token 流,小到解析一个配置文件,都很有帮助;而用 Python 实现一个支持自定义规则的类 Flex/Lex 词法分析器只需要短短 56 行代码,简单拷贝粘贴到你的代码里,让你的代码具备基于可定制规则的词法分析功能。

原理很简单,熟读 Python 文档的同学应该看过 regex module 帮助页面最下面有段程序:

def tokenize(code):
    keywords = {'IF', 'THEN', 'ENDIF', 'FOR', 'NEXT', 'GOSUB', 'RETURN'}
    token_specification = [
        ('NUMBER',   r'\d+(\.\d*)?'),  # Integer or decimal number
        ('ASSIGN',   r':='),           # Assignment operator
        ('END',      r';'),            # Statement terminator
        ('ID',       r'[A-Za-z]+'),    # Identifiers
        ('OP',       r'[+\-*/]'),      # Arithmetic operators
        ('NEWLINE',  r'\n'),           # Line endings
        ('SKIP',     r'[ \t]+'),       # Skip over spaces and tabs
        ('MISMATCH', r'.'),            # Any other character
    ]
    tok_regex = '|'.join('(?P<%s>%s)' % pair for pair in token_specification)
    line_num = 1
    line_start = 0
    for mo in re.finditer(tok_regex, code):
        kind = mo.lastgroup
        value = mo.group()
        column = mo.start() - line_start
        if kind == 'NUMBER':
            value = float(value) if '.' in value else int(value)
        elif kind == 'ID' and value in keywords:
            kind = value
        elif kind == 'NEWLINE':
            line_start = mo.end()
            line_num += 1
            continue
        elif kind == 'SKIP':
            continue
        elif kind == 'MISMATCH':
            raise RuntimeError(f'{value!r} unexpected on line {line_num}')
        yield Token(kind, value, line_num, column)

上面这个官方文档里的程序,输入一段代码,返回 token 的:名称、原始文本、行号、列号 等。

它其实已经具备好三个重要功能了:1)规则自定义;2)由上往下匹配规则;3)使用生成器,逐步返回结果,而不是一次性处理好再返回,这个很重要,可以保证语法分析器边分析边指导词法分析器做一些精细化分析。

我们再它的基础上再修改一下,主要补充:

  • 支持外部传入规则,而不是像上面那样写死的。
  • 规则支持传入函数,这样可以根据结果进行二次判断。
  • 更好的行和列信息统计,不依赖 NEWLINE 规则的存在。
  • 支持 flex/lex 中的 “忽略”规则,比如忽略空格和换行,或者忽略注释。
  • 支持在流末尾添加一个 EOF 符号,某些 parsing 技术需要输入流末尾插入一个名为 \$ 的结束符。

对文档中的简陋例子做完上面五项修改,我们即可得到一个通用的基于规则的词法分析器。

改写后代码很短,只有 56 行:

(点击 more 展开)

Continue reading

Loading

Posted in 编译原理 | Tagged | 6 Comments

帧同步游戏中使用 Run-Ahead 隐藏输入延迟

帧同步可以轻松解决高互动的联网游戏(如格斗,RTS 等)的同步问题,但该方案对延迟很敏感,现在一般省内服务器延迟差不多 10-15ms (1帧),跨省一般 40ms (2-3 帧),在此情况下,使用 Run-Ahead 机制可以有效的掩盖延迟的体感,让用玩家立马看到自己的操作反馈。

该机制有很多其他名字比如:预测回滚(prediction and rollback),或者时间曲力(time warp),名字取的天花乱坠的,很多文章也只是云里雾里说一半天,结果还没说清楚,所以本文打算最简短的句子说清楚这个概念,并给出可以实际操作的实现步骤。

我觉得用 Run-Ahead 这个质朴的名字更容易说明这个算法背后的思想:提前运行,这个概念不光用在游戏同步里,也早已用在游戏模拟器中,为了便于理解,先说一下模拟器中的情况(更简单)。

RetroArch 使用 Run-Ahead 隐藏输入延迟,一般需要设置一下 Run-Ahead 的帧数,比如 0 是关闭,1 是提前运行一帧,2 是提前运行两帧,一般设置用 1 或者 2,不要超过 5,因为太高游戏表现会很奇怪:

运行时 RetroArch 为每帧保存快照,假定的是用户输入有持续性,那么运行时当前帧使用上一帧用户的输入作为本帧输入(假设 runahead 设置为 1),然后接着往下运行,如果用户新输入来了,一律把它算作当前帧-1 的输入,然后再去对比历史如果和上一帧所尝试假定的输入一致就继续,否则快照回退到上一帧,重新用新的输入去运行,然后再快进到当前帧。

通常手柄或键盘都有 5ms 左右的输入延迟(部分设备如 switch 的 pro 手柄延迟高达 15ms),再加上操作系统处理的延迟,投递到模拟器进程里,从按下到真正开始处理也许也差不多 1 帧的时间了,RetroArch 用这个功能,也只有用户真实输入和预测输入不一致时才会触发,由于间隔很短,所以即使纠正也难看出来,最终在模拟器上达到了物理设备一样的超低延迟体验。

理解了模拟器的 Run-Ahead 实现,其实在帧同步里的原理也就差不多了,无外乎是用远程的旧输入,搭配本地刚采集到的新输入,作为预测帧的输入值,产生新帧,不匹配了再回滚。

帧同步里引入类似 Run-Ahead 的机制,要求游戏最近所有状态都可以被快速保存、复制和恢复,实现有很多种,你可以用状态的反复前进、后退来实现,但是 BUG 率太高了,这里给出一个更简易的实现方式:

(点击 more 展开)

Continue reading

Loading

Posted in 游戏开发 | Tagged | 3 Comments

CD2:SharpDevelop

SharpDevelop 是 .Net Framework 时代最受欢迎的轻量级 IDE,它代替庞大的 Visual Studio使用 C# / VB.Net 开发各种 .Net Framwork 4.5 之前的程序(WinForm,控制台,WPF,组件程序),而本身大小却只有 20MB,且运行比 VS 流畅,深受大家喜欢:

虽然只有 20MB 却包含完整的可视化设计器可以拖拽控件,.Net Framework 4.5 虽然不够现代,但是确是 .Net Core 前较新的一个版本,语言方面支持到 C# 5.0(包含 async/await 那个版本),并且操作系统兼容性很好,能从 Windows XP 一路兼容到 Windows 11。

本光盘兼具收藏价值和实用价值,包含了 SharpDevelop IDE 本身和多个版本的 .Net Framework 运行时环境,以及相关配套资源,当你想开发一两个小工具和轻量级的桌面应用时,无需下载安装庞大的 Visual Studio,小巧的 SharpDevelop 就能帮你迅速搞定。

SharpDevelop 被人喜爱除了免费开源外,还有个重要原因,就是启动快,运行丝滑流畅,根本不会像 VS 那样时不时卡一下,这为其赢得了不少用户。

(点击 more/continue 继续)

Continue reading

Loading

Posted in 未分类 | Tagged | Leave a comment

库代码中是否应该检查 malloc 的返回值?

现在网上有很多似是而非的观点,比如 malloc 失败不用处理,直接退出,这样的经验看似很聪明,实际却很局限,比如:

1)嵌入式设备:不是所有设备都能有一个强大的全功能的操作系统,也不是所有设备都能有虚拟内存功能。

2)长时间运行的程序,内存虽然够,但由于碎片,会导致没有连续足够大的线性地址进而分配失败,32 位程序特别明显。

3)管理员对某类进程设置过内存限制,比如某类要起很多个的进程,那么设置一下内存限制是一个很正常的操作。

4)程序通过容器运行,事先给定了最大内存用量。

5)操作系统 overcommit 选项被管理员关闭。

所以说 malloc 失败不用检测,大多数是明确知道自己运行环境的某一类程序,比如上层业务,比如 CRUD,比如你就是做个增删改查,知道自己运行于一台标准的 linux 服务器,那么确实无需多处理,崩了也就崩了;或者你的程序严重依赖 malloc 三行代码一次分配,那么即使恢复出来估计你也很难往下走,不如乘早崩溃,免得祸害他人。

上面这些情况确实可以简单粗暴处理,但如果你开发一些基础库,你没办法决定自己是会运行在一台标准服务器上还是某个小设备里,那么上面的经验就完全失效了。

比如你开发一个类似 libjpeg 的图象编解码库,你没法假定运行环境,那么 malloc 失败时,你是该自作主张直接 assert 强退掉呢?还是该先把分配了一半的各种资源释放干净,然后向上层返回错误码,由上层决定怎么处理比较好呢?比如上层可选择:1)报错退出;2)释放部分缓存后再运行;3)降级运行,比如图象编码使用 low profile 再运行一遍。

你作为图象编码库需要经常分配一些比较大块的内存,你这里失败了,不代表上层无法继续分配内存处理后续任务对不对?上层内存里有很多图片 cache 用于加速图片显示,你这里失败了,上层感知到,直接从 cache 里回收一波,内存就又有了,对不对?图象视频编码根据资源消耗高低都有 high profile, low profile 的运行模式,你 high profile 内存不够了报告上层,上层视情况还可以选择再用 low profile 跑一遍对不对?

就是上层什么都不做,只在检测到你的返回值时在日志里记录一行内存不够再退出,你也得给上层一个选择的权利,而不是不管不顾,直接退掉;所以,你一个库碰到这类问题还是要自己处理干净把错误返回给上层,让上层来判断该如何处理更恰当一些。

再一种是需要精细控制内存的应用层业务,比如自己主动管理多个内存池,栈上的池不够了就应该到堆上的堆上池分配,堆上池分配不了就应该回收,回收不了就向操作系统要新的,这种情况也不能一概而论。

那么是不是说所有的 malloc 都要去处理 NULL 返回?当然不是,这里强调的是你既然选择用 C 语言了,脑袋里要有根弦,知道这个问题需要分情况处理,区分得了哪些代码重要,哪些代码不重要;哪些代码要什么力道去写,写到多深,这样即使某些业务模块懒得处理了,不处理 NULL 或者写个 assert 也没问题,但如果头脑里没这跟弦,不知道不同层次的代码需要不同的力道去写,一概无脑的不处理或者 assert,那么这样的代码写多了,只会越写越笨。

Loading

Posted in 编程技术 | Tagged | Leave a comment

互联网技术比游戏后端技术领先十年吗?

最近时间线上又起了一场不大不小的论战,做互联网的人觉得游戏服务端发展很慢,同时互联网技术日新月异,似乎觉得互联网技术领先了游戏后端技术十年,这个结论显然是武断的,几位朋友也已经驳斥的很充分了,游戏服务端的同学实属没必要和这个互联网的人一般见识,本来就此打住也还挺好。

但最近两天事情似乎正在悄悄起变化,时间线上一直看到不停的有人跳出来,清一色的全在说互联网简单,什么做个电商不过就是 CRUD 的话也出来了,看的我也大跌眼镜,过犹不及吧。

今天更是又刷到有几位不管不顾就说什么游戏服务端领先互联网十年什么的,似乎这又要成为了另外一个极端了,那么有几点情况是不是也请正视一下:

1)游戏服务端足够复杂,但是发展太慢,祖传代码修修补补跑个十多年的不要太多。能用固然是好事,但没有新观念的引入,导致可用性和开发效率一直没有太多提升。

2)各自闭门造车,没有形成行业标准与合力,这个项目的代码,很难在另一个项目共享,相互之间缺少支持和协同。

3)互联网后端随便拎出一个服务来(包括各种 C/C++ 基建)大概率都没有游戏服务端复杂,但最近十年日新月异,形成了很强的互相组合互相增强的态势。

我上面指的是互联网基建项目,不是互联网 CRUD,互联网近十年的发展,让其整体可用性,效能,开发效率,都上了很多个台阶,不应一味忽视。

如果继续觉得游戏服务端领先互联网十年可以直接右转了,开放心态的话我也可以多聊一些(点击下方 more 阅读更多):

Continue reading

Loading

Posted in 游戏开发, 网络编程 | Tagged , , | 5 Comments

使用 LIBLR 解析带注释的 JSON

前文《基于 LR(1) 和 LALR 的 Parser Generator》里介绍了春节期间开发的小玩具 LIBLR ,今天春节最后一天,用它跑一个小例子,解析带注释的 json 文件。由于 python 自带 json 库不支持带注释的 json 解析,而 vscode 里大量带注释的 json 没法解析,所以我们先写个文法,保存为 json.txt

# 定义两个终结符
%token NUMBER
%token STRING

start: value                {get1}
     ;

value: object               {get1}
     | array                {get1}
     | STRING               {get_string}
     | NUMBER               {get_number}
     | 'true'               {get_true}
     | 'false'              {get_false}
     | 'null'               {get_null}
     ;

array: '[' array_items ']'                  {get_array}
     ;

array_items: array_items ',' value          {list_many}
           | value                          {list_one}
           |                                {list_empty}
           ;

object: '{' object_items '}'                {get_object}
      ;

object_items: object_items ',' item_pair    {list_many}
            | item_pair                     {list_one}
            |                               {list_empty}
            ;

item_pair: STRING ':' value                 {item_pair}
         ;

# 词法:忽略空白
@ignore [ \r\n\t]*

# 词法:忽略注释
@ignore //.*

# 词法:匹配 NUMBER 和 STRING
@match NUMBER [+-]?\d+(\.\d*)?
@match STRING "(?:\\.|[^"\\])*"

有了文法,程序就很短了,50 多行足够:(点击 more 展开)

Continue reading

Loading

Posted in 编译原理 | Tagged | 6 Comments