TimothyQiu's Blog

keep it simple stupid

Rust & Zig 猜数字

以前囫囵吞枣学了一下 Rust 以及 Zig,但忘得快差不多了。最近有空闲准备再重新看一下。

迫使我有重新学习使用 Rust / Zig 的原因,主要还是 CMakeLists.txt 写起来太麻烦了。虽然 C/C++ 也有 xmake 这样用起来更加方便的工具,但对于我个人来说,总觉得味道怪怪的,用第三方库也不甚方便。


昨天把 The Rust Programming Language 里的猜数字游戏用 Rust 和 Zig 都写了一下。

Rust 版本:

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {guess}");

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

Zig 版本:

const std = @import("std");

pub fn main() !void {
    const stdout = std.io.getStdOut().writer();
    const stdin = std.io.getStdIn().reader();

    try stdout.print("Guess the number!\n", .{});

    var prng = std.rand.DefaultPrng.init(blk: {
        var seed: u64 = undefined;
        try std.os.getrandom(std.mem.asBytes(&seed));
        break :blk seed;
    });

    const secret_number = prng.random().intRangeAtMost(i32, 1, 100);

    try stdout.print("The secret number is: {}\n", .{secret_number});

    while (true) {
        try stdout.print("Please input your guess.\n", .{});

        var buffer: [128]u8 = undefined;
        const guess_line = stdin.readUntilDelimiter(&buffer, '\n') catch {
            @panic("Failed to read line");
        };
        const guess = std.fmt.parseInt(i32, std.mem.trim(u8, guess_line, &std.ascii.whitespace), 10) catch {
            continue;
        };

        try stdout.print("Your guess is: {}\n", .{guess});

        if (secret_number > guess) {
            try stdout.print("Too small!\n", .{});
        } else if (secret_number < guess) {
            try stdout.print("Too big!\n", .{});
        } else {
            try stdout.print("You win!\n", .{});
            break;
        }
    }
}

写完感觉挺符合我对它们俩的预期的,一个属于更好的 C++、一个属于更好的 C。

在这个例子里,虽然 Zig 的代码更加 explicit,但是我觉得写起来更舒服,原因有两点:

首先,必须显式进行错误处理。有出错机会的函数调用都需要使用 try 把错误抛给上层,或者使用 catch 原地处理。

其次,需要显式指定内存分配规则。比如 readUntilDelimiter 只能接受固定大小的缓冲,否则就需要换成自动分配内存的版本 readUntilDelimiterAlloc,而这个版本也需要你传入允许分配的内存上限作为参数。所以你会意识到,允许输入「无限长」的字符串是没有意义、甚至是危险的,需要定一个合理的上限,从而开始考虑修改业务逻辑的设计。

当然,这个例子显然非常基础,还无法体现出内存安全的必要性。

以上。

给 Arch 添加 Swap 文件

自从半年前换了 Arch Linux,就一直没有设置 swap,心想最多也就编译的时候 Chrome 标签页「哦哟」一下嘛。

不过实际没有 swap 不能休眠还是有点不踏实,毕竟是台式机,又懒得接 UPS。于是以下就是这周末设置 swap 文件用于休眠的历程(内容 Arch Wiki 上都有提及,就是总结记录一下)。

查看当前 Swap 状态

使用 swapon --show

创建多大的 Swap 文件

Swap 文件的大小一般至少是 512M。如果你和我一样是为了让系统能够休眠,那么可以参考 /sys/power/image_size 里的字节数,如果你没有修改过,这个值默认是当前内存大小的五分之二。

/sys/power/image_size 控制的就是休眠镜像的文件大小。系统会尽可能保证镜像大小不超过这个值,即便无法实现,也会尽量将镜像缩小。也就是说,你给它设 0 也是可以的,此时休眠镜像的大小会是最小的。

创建 Swap 文件

首先在一些文件系统上使用 swap 文件是会有问题的,比如早期的 Btrfs 就不支持。

创建 Swap 文件有很多种方法,但最可移植的方法是使用 dd。比如创建 1G 的 /swapfile

# dd if=/dev/zero of=/swapfile bs=1M count=1024 status=progress
# chmod 0600 /swapfile
# mkswap -U clear /swapfile

这里的 chmod 是为了安全,所有人都可读写的 Swap 文件是巨大隐患。

mkswap-U 参数用于设置 swap 的 UUID。但是因为 swap 文件必须使用文件系统路径去指定,所以这里使用特殊的 clear 作为参数去清空它(实际效果是设置了全零的 UUID)。

打开与关闭

创建好 swap 文件后,就可以直接启用了:

# swapon /swapfile

最后把它放进 /etc/fstab 里,这样每次启动就会直接启用:

/swapfile none swap defaults 0 0

如果是要关闭,顾名思义就:

# swapoff /swapfile

关闭以后它就是个普通文件,想删除就可以直接删除了。

使用 Swap 文件进行休眠

故事讲到这里,休眠和混合睡眠应该就都可以创建休眠镜像了。但我们还需要进行额外设置,才能在启动时使用这个休眠镜像。

内核参数

首先需要为内核设置 resumeresume_offset 参数,告诉它去哪里找休眠镜像。

resume 参数是 swap 文件所在的分区(例如 /dev/nvme0n1p2,或者 UUID=4209c845-f495-4c43-8a03-5363dd433153),可以在 /etc/fstab 里查看,也可以通过 findmnt -no UUID -T /swapfile 获取。

resume_offset 是文件开头在这个分区中的物理偏移量,可以通过 filefrag -v /swapfile 查看。

关于内核参数的设置,如果你和我一样使用的是 GRUB,可以编辑 /etc/default/grub,把 resume=XXX resume_offset=XXX 追加到 GRUB_CMDLINE_LINUX_DEFAULT 里。最后重新生成一下 grub.cfg 即可:

# grub-mkconfig -o /boot/grub/grub.cfg

如果你有闲情逸致,也可以先不这么做。直接在 GRUB 启动界面按 e,然后去手动编辑本次启动所使用的参数 😛

initramfs

如果你的 initramfs 没有使用 systemd 钩子(使用的是 base),那么就还需要添加一个 resume 钩子才会尝试从休眠中恢复:

编辑 /etc/mkinitcpio.conf 文件,在 HOOKS=(...) 里的 udev 之后的任何位置加入 resume。最后重新生成 initramfs 即可:

# mkinitcpio -p linux

参考

Arch Linux 下 Display Manager 黑屏

最近 MBP 电池彻底坏了不想修,换 Windows 用了几天又很不习惯,于是就装了个 Arch。

用 Arch Linux 得自行装图形界面、使用 Display Manager 进行图形化的登录操作。但这时候就遇到个很好玩的事情:进 Display Manager 时黑屏(如果去 BIOS 里绕一下,有小概率不黑屏),不过在黑屏里 Ctrl+Alt+F2 还是可以切到别的 tty 的。

内核参数、驱动之类的检查一圈都没有问题,把我用的 LightDM 换成别的 Display Manager 也还是如此。最后在万能的 Arch Wiki 找到了解决方法,在 /etc/lightdm/lightdm.conf 里配置一下:

[LightDM]
logind-check-graphical=true

黑屏的原因是,图形驱动还没加载完,LightDM 就启动了:系统启动得太快了,得显式让 LightDM 等待图形设备 🤣

凤凰点阵体

做像素风/复古游戏不免要用上点阵字体,之前我用过一阵子 IPix,但它时有缺字,一些英文、数字、以及标点符号的样子和字间距给人感觉也有点奇怪。于是我就自己做了一个像素字体。

做英文字体和中文字体所需要涉及的字形不在一个数量级上,前者撑死了一百个左右,后者则至少要覆盖常用的几千个汉字,非常体力活。IPix 的字模来自于开源项目 BitmapFont,这个项目的作者收集了很多远古系统里的字模数据(感觉从老系统里提取的字模在版权方面界限还是有点模糊),可以直接生成对应的字形。

这些字模数据中,每一个字符都对应一系列的比特位。比如 16 像素宋体的「人」字在数据文件中对应 01000100010001000100010001000280028002800440044008201010200e4004,按照每 16 个比特位换行表示后就成了点阵的「人」字:

       X
       X
       X
       X
       X
       X
       X
      X X
      X X
      X X
     X   X
     X   X
    X     X
   X       X
  X         XXX
 X           X

有了这样的点阵数据,我们就可以很繁琐方便地制作各种格式的字体文件了。

我把制作好的 TrueType 字体放到了 itch.io 上,有需要的童鞋可以自取:

p.s. 其实市面上有一款非常不错的收费点阵字体 丁卯点阵体,支持 7px 和 9px 的大小。

花10分钟用Godot开发平台跳跃游戏

10 分钟的内容,录了两天。一边解说一边写代码,然后还得控制在 10 分钟左右真是太难了,深刻体会到了舌头打结的快感。

BV1Xi4y1M7Eq

p.s. 上一次 B 站投稿居然已经是在八年前了。