普通视图

发现新文章,点击刷新页面。
昨天以前Skywind Inside

使用 rclone bisync 两步搭建个人云盘

作者 skywind3000
2026年1月30日 00:27

如何最快搭建个人云盘?Nextcloud 太大太臃肿,后面一堆 apache/nginx/php/mysql,命令行同步也不好用,私货还太多。

著名的 syncthing 要求有 P2P 中转服务;还要求同步时另一台电脑也开着机,不开机的话,你要在服务器上做一个同步点持续运行,那么做了这个服务和 C/S 架构的 rclone 有啥区别呢?还要多依赖一个 P2P 中转服务(虽然可以找到别人提供的),其他的开源产品也好不到哪里,相比之下 rclone 是最轻量级的解决方案了。

恰巧 rclone 最近几年一直在优化 bisync 功能,就是双向同步,网盘的核心功能,bisync 在 2022 年引入 rclone 一直处于 experimental/beta 阶段,其实几年前就基本可以用了,官方文档在 2025 年下半年正式移除 experimental/beta 字样,然后 bisync 功能正式转正。

所以我们用 rclone 的 bisync 功能来分别搭建客户端和服务端:

1️⃣ 服务端搭建

使用 supervisor 编辑 /etc/supervisor/conf.d/rclone-sftp.conf 文件:

[program:rclone-sftp]
command=/home/data/app/rclone/rclone serve sftp /home/data/sync --addr :2022 --user MYNAME --pass MYPASS --cache-dir /var/cache/rclone --vfs-cache-mode full
user=nobody
autorestart=true

然后重启:

sudo supervisorctl reload

一个 rclone 提供的 sftp 服务就启动了,就是这么简单,它速度快性能好,传输有加密。

最后我们在 sftp 的文件夹里建立一个叫做 bisync 的文件夹:

sudo mkdir /home/data/sync/bisync
sudo chown -R nobody:nogroup /home/data/sync

处理好权限,让 nobody 这个用户能够读写即可,就这么几步操作,比搭建个 Nextcloud 之类的服务方便太多了。

2️⃣ 客户端

我用 WSL,编辑配置 /root/.config/rclone/rclone.conf 内容如下:

[sftp1]
type = sftp
host = 192.168.1.12
user = MYNAME
port = 2022
pass = [运行 rclone obscure "MYPASS" 得到的加密字符串]
shell_type = unix
md5sum_command = md5sum
sha1sum_command = sha1sum

不想手工填写也可以用 rclone config 交互式的初始化并连接 sftp 服务,一旦服务配置好了,我么就能找个文件夹然后在定时任务里 bisync 了。

不过首先要初始化一下:

mkdir /mnt/e/Local/Cloud/rclone
rclone bisync /mnt/e/Local/Cloud/rclone sftp1:/bisync --resync

这个 --resync 参数用于首次同步初始化,后面就定时任务了。

编辑同步脚本 /home/data/shim/rclone-bisync-sftp1 内容如下:

#!/bin/bash

# configure
RCLONE_BIN="/home/data/app/rclone/rclone"
RCLONE_REMOTE=sftp1:/bisync
RCLONE_LOCAL=/mnt/e/Local/Cloud/rclone
LOGFILE="/var/log/rclone-bisync.log"

# start
$RCLONE_BIN bisync "$RCLONE_LOCAL" "$RCLONE_REMOTE" \
        --create-empty-src-dirs \
        --compare size,modtime \
        --resilient \
        --recover \
        --max-lock 2m \
        --verbose \
        --log-file="$LOGFILE"

定时启动:

编辑 /etc/cron.d/rclone_bisync 内容如下:

*/5 * * * * root /home/data/shim/rclone-bisync-sftp1

即可,调试完正常可以把启动脚本里的 --verbose 去掉,没错误就不写日志了。你可以在多个客户端上进行类似的配置,不管 Windows 还是 Linux,要点就是先 config 上远端 sftp,然后初始化 --resync,然后就是定时调用 bisync 子命令就行。

然后你就得到了一个干净纯粹的,开源的,多端同步网盘了。

后记

在 bisync 之前,rclone 实现网盘主要靠 mount,这个功能类似 samba,每次读取写入都要走网络,局域网还行,跨机房就卡的不行了,它后来提供了 cache 机制有一定缓解,但一致性又有问题,经常不同步,这边改了不能像 samba 那样通知那边。

所以 rclone 做网盘体验最好的目前就是 bisync 功能了,bisync 的 bisync 可以用很多源,作为服务端,如果你不想搭建一大套 webserver / fileserver 来做源的话,最好就是用 rclone 自己可以 serve 的服务,包括:http,sftp,webdav,nfs 等等。

我试过 webdav 等,发现如果想要加密,前面还得再套一层 https 代理,或者又搞一大堆证书什么的,异常麻烦,所以体验最好,速度最快的是 sftp 服务。

Loading

The post 使用 rclone bisync 两步搭建个人云盘 appeared first on Skywind Inside.

单头文件 C++ 游戏开发库(GameLib.h)

作者 skywind3000
2026年4月3日 22:48

最近小孩在学 C++(信奥),学了堆语法以后不知道干嘛,学了一年也只会对着黑窗口打印内容,正反馈太弱了,不像其他语言,学个几天就能做出漂亮的东西来,恰巧他对开发游戏感兴趣,我扫了一眼现在 C++ 的游戏开发框架,都太复杂了,SDL 概念琐碎,SFML 使用麻烦,所以我写了个针对初学者的游戏库,只有一个头文件 GameLib.h 零依赖,拷贝到代码目录 include 就能用,十行代码就能出个小 demo:

本着简化一切的思想,使用起来比 PyGame 还要简单:

#include "GameLib.h"

int main() {
    GameLib game;
    game.Open(640, 480, "My Game", true);

    int x = 320, y = 240;

    while (!game.IsClosed()) {
        if (game.IsKeyDown(KEY_LEFT))  x -= 3;
        if (game.IsKeyDown(KEY_RIGHT)) x += 3;
        if (game.IsKeyDown(KEY_UP))    y -= 3;
        if (game.IsKeyDown(KEY_DOWN))  y += 3;

        game.Clear(COLOR_BLACK);
        game.FillCircle(x, y, 15, COLOR_CYAN);
        game.DrawText(10, 10, "Up/Down/Left/Right to move!", COLOR_WHITE);
        game.Update();
        game.WaitFrame(60);
    }
    return 0;
}

就这么几行代码,没有 SDL 里反锁的像素格式,消息机制,各种乱七八糟 SDL_ 开头的对象,也没有初始化就需要 500 行代码的 DirectX 那么麻烦,所有复杂的东西藏起来,只留下做游戏的乐趣。

编译:

g++ main.cpp -o game.exe

不需要加任何编译参数,很多初学者连命令行编译都不懂(比如我家小孩),只会直接在 DevC++ 里点编译+运行,让他们像用其他库一样添加一些类似 -ld3d9x 之类编译参数,可能直接就劝退一大群人,因此这个库完全使用默认编译参数,所有依赖都是动态库自己手工加载。

运行就能用方向键控制小球移动:

几行代码迅速看到反馈。

为什么做这个库呢?

市面上的游戏库(SDL、SFML、raylib)都很好,但对于刚接触 C++ 的初学者来说:

  • SDL 要配置头文件路径、链接十几个 dll,使用复杂
  • SFML 要用 CMake
  • raylib 需要熟练掌握线性代数,三维图形基础知识,熟练 C/C++,对初学者太不友好

GameLib 的目标是零门槛:把 GameLib.h 拷到项目文件夹,写一个 .cpp 文件,点编译,游戏就跑起来了。

它专门为 Dev C++(很多学校编程课在用的 IDE)设计,兼容其自带的 GCC 4.9.2 编译器。当然,任何支持 C++11 的 Windows 编译器都可以用。

(点击下面展开更多)

特性一览

零配置:

  • 单个头文件 GameLib.h,拷贝即用
  • 不依赖 SDL / SFML / DirectX / OpenGL
  • 编译参数都不需要加(全动态加载),可选择性添加 -mwindows 参数
  • 兼容 Dev C++ 自带的 GCC 4.9.2

开箱即用的绘图:

  • 画点、线、矩形、圆、三角形(描边和填充)
  • 内嵌 8×8 像素点阵字体,支持所有可打印 ASCII 字符
  • DrawPrintf 像 printf 一样在屏幕上格式化输出
  • 所有图形算法自行实现(Bresenham 直线、中点圆、扫描线填充)

精灵系统:

  • 加载 PNG、JPG、BMP、GIF 等格式
  • 支持 8-bit 调色板、24-bit、32-bit 图片(自动转换为 32 位 ARGB)
  • 24 位图片自动补全 alpha 通道(设为不透明)
  • 翻转、Color Key 透明、Alpha 混合、区域裁剪绘制
  • 用整数 ID 管理,不需要理解指针和对象生命周期

键盘和鼠标:

  • IsKeyDown — 按住检测
  • IsKeyPressed — 单次按下检测(按下瞬间触发一次)
  • 鼠标位置和三键状态
  • 预定义所有常用按键常量:KEY_AKEY_Z、方向键、F1F12

声音:

  • PlayWAV — 播放音效(WAV 格式,异步)
  • PlayMusic / StopMusic — 播放背景音乐(MP3/MIDI,基于 MCI)
  • 音效和音乐独立通道,互不干扰

游戏工具:

  • Random(min, max) — 随机数
  • RectOverlap / CircleOverlap — 碰撞检测
  • Distance — 两点距离
  • DrawGrid / FillCell — 网格绘制(适合俄罗斯方块、棋盘类游戏)
  • GetDeltaTime / GetFPS — 帧时间和帧率

Tilemap 系统:

  • CreateTilemap — 用 tileset 精灵创建瓦片地图
  • SetTile / GetTile — 设置和读取瓦片
  • DrawTilemap — 绘制地图,支持不透明、Color Key、Alpha 三种模式
  • 只绘制屏幕可见范围内的瓦片,大地图也不卡
  • 配合相机偏移轻松实现横版卷轴和视差滚动

快速上手

第一步:下载

GameLib.h 放到你的项目文件夹里。

第二步: 写代码

创建一个 main.cpp:

#include "GameLib.h"

int main() {
    GameLib game;
    game.Open(800, 600, "Hello GameLib", true);

    while (!game.IsClosed()) {
        game.Clear(COLOR_DARK_BLUE);
        game.DrawTextScale(200, 250, "Hello, World!", COLOR_GOLD, 3);
        game.DrawText(280, 320, "Press ESC to exit", COLOR_GRAY);

        if (game.IsKeyPressed(KEY_ESCAPE)) break;

        game.Update();
        game.WaitFrame(60);
    }
    return 0;
}

第三步:编译运行

Dev C++:新建项目 > 添加 main.cpp > 编译运行。

或者命令行:

g++ -o game.exe main.cpp -mwindows

就行了。

随机星空

再来个有点视觉效果的例子,随机星空:

#include "GameLib.h"

int main() {
    GameLib game;
    game.Open(800, 600, "Starfield", true);

    // 生成 200 颗星星
    int sx[200], sy[200], speed[200];
    uint32_t colors[] = {COLOR_WHITE, COLOR_LIGHT_GRAY, COLOR_YELLOW, COLOR_CYAN};
    for (int i = 0; i < 200; i++) {
        sx[i] = GameLib::Random(0, 799);
        sy[i] = GameLib::Random(0, 599);
        speed[i] = GameLib::Random(1, 5);
    }

    while (!game.IsClosed()) {
        game.Clear(COLOR_BLACK);

        for (int i = 0; i < 200; i++) {
            sx[i] -= speed[i];
            if (sx[i] < 0) {
                sx[i] = 800;
                sy[i] = GameLib::Random(0, 599);
            }
            game.SetPixel(sx[i], sy[i], colors[speed[i] % 4]);
        }

        game.DrawText(250, 290, "Press ESC to exit", COLOR_GRAY);
        if (game.IsKeyPressed(KEY_ESCAPE)) break;

        game.Update();
        game.WaitFrame(60);
    }
    return 0;
}

就这么简单,运行效果(点击播放 GIF 动画):

几行代码就能有看得见摸得着的东西,正反馈远远强过黑窗口打印文字。

这个 GameLib.h 适合开发哪些类型的游戏呢?

  • 太空射击 (Space Shooter)
  • 横版卷轴 (Side-Scrolling Platformer)
  • 俄罗斯方块 (Tetris)
  • 贪吃蛇 (Snake)
  • 打砖块 (Breakout)
  • 走迷宫 (Maze)
  • 接水果 (Fruit Catcher)
  • 弹幕游戏 (Bullet Hell)
  • 画板程序 (Paint)
  • 任何你能想到的 2D 小游戏

更多例子请访问项目主页:

https://github.com/skywind3000/GameLib

里面有 15 个例子程序,包括类似《打砖块》 ,《太空射击》之类游戏的代码:

一步一步教你从:窗口控制,键盘鼠标交互,图形绘制,精灵,音乐,卷轴等覆盖各个 GameLib.h 的功能点。

欢迎学会了 C++ 语法想做点什么的同学尝试。

Loading

The post 单头文件 C++ 游戏开发库(GameLib.h) appeared first on Skywind Inside.

C++ 最好用的 TUI 界面库 Turbo Vision 2.0

作者 skywind3000
2026年1月3日 22:06

说起文本模式界面库,也许有人听过 Turbo Vision 2.0,这货是当年 Borland C++ 3.1 (不是 Turbo C 2.0)背后的商用界面库 TV2 的开源版,经过多年迭代,如今是一款支持 unicode 和现代 C++ 跨平台的 TUI 库了:

项目地址:https://github.com/magiblot/tvision

主要特性:

  • 跨平台:Windows,Linux,macOS,DOS,FreeBSD
  • 支持鼠标,支持 unicode,正常显示中文;
  • 多窗口:窗口可以自由拖拽移动,扩大缩小,全屏,互相覆盖(overlap)
  • 控件多:主菜单,对话框,checkbox, radiobox,dropdown list,文件选择器,进度条,阴影
  • 文本编辑:语法高亮,自动 indent,鼠标选中,shift+方向键选中,CTRL+C/CTRL+V
  • 支持真彩:原来老的 Borland 版本的 TV2 只支持 16 色。

Linux 下大部分 TUI 程序都是平铺窗口,无法重叠,更无法自由拖动,而 Turbo Vision 2.0 完全像用图形界面程序一样,鼠标操作这些窗口自由移动,扩大缩小,全屏化:

而且 Linux 下面大部分程序对 ALT+ ,CTRL+ ,SHIFT+CTRL+ 等组合键支持非常有限,而 TV2 对不同平台的键盘鼠标做了很好的兼容,让你在远程终端里也能自由的使用各种组合键和功能键。

用过 Borland C++ 3.1 的人一定会以为 BC31 重生了,某种意义上来讲是的,不要觉得技术古老,Linux 下面 TUI 发展几十年,没一个打得过 Turbo Vision 2 的:

这是 TV2 做的终端模拟器 tvterm,各位天天用 tmux 分屏模拟,但 tmux 发展了那么长时间都不支持子窗口互相重叠覆盖,鼠标拖动,TV2 可以让你像用桌面软件一样灵活的操作各个子窗口。

Vim/NeoVim 直到 2019 年才支持 popup/floatwin 可以实现上面类似的效果,而 Turbo Vision 2 在三十年前就做到了。

过去 TV2 只支持 ANSI 编码,无法显示中文,如今中文,日文都能正常显示:

包括 emoji:

你在 Linux 下大部分 TUI 程序想调整下设置都只能编写配置文件,配置 vim 也只能写 vimrc,但 TV2 里有丰富的 TUI 设置对话框:

大部分设置,对话框里点点鼠标就能搞定,毫无学习门槛,无需看很多帮助才知道怎么写配置:

当然 TV2 也有内置帮助系统,它有一套类似 .chm 等帮助系统,内部包含索引,跳转,可以在 TUI 内随时查看你的帮助文件,比如下面的快捷键帮助,基本上和 Windows 程序一脉相承:

最后是编程接口,linux 下一堆 tui 接口,比如 ncurses 这样的都是比较初级的原始的,没有任何高级控件,同时接口也是 C 的,而 TV2.0 基本上所有高级控件你都可以直接使用,写起代码来类似传统的 Qt 程序:

class HelloApp : public TApplication {
public:
    HelloApp();
    void showHelloDialog();
};

HelloApp::HelloApp() : 
    TProgInit(&TApplication::initStatusLine,
              &TApplication::initMenuBar,
              &TApplication::initDeskTop) {
    // 启动时显示对话框
    showHelloDialog();
}

void HelloApp::showHelloDialog() {
    // 创建对话框 (x, y, width, height)
    TDialog *dialog = new TDialog(TRect(0, 0, 40, 9), "Hello World");

    if (dialog) {
        // 居中对话框
        dialog->options |= ofCentered;

        // 添加 "Hello World" 静态文本 (居中显示)
        TStaticText *text = new TStaticText(
            TRect(2, 2, 38, 5), 
            "\003Hello World"  // \003 表示居中对齐
        );
        dialog->insert(text);

        // 添加 OK 按钮 (居中)
        TButton *button = new TButton(
            TRect(14, 5, 26, 7),
            "~O~K",
            cmOK,
            bfDefault
        );
        dialog->insert(button);

        // 显示模态对话框
        deskTop->execView(dialog);

        // 清理
        TObject::destroy(dialog);
    }
}

int main() {
    HelloApp app;
    app.run();
    app.shutDown();
    return 0;
}

基本就是类似 Qt 的编程方式:事件驱动+控件组合+OOP,不是 ncurses 那种 select/read 标准输入,自己解码半天终端控制码,在琐碎的控制各处显示,自己控制状态切换和重绘,写的你想吐。

Linux 下面基于 ncurses 二次封装的库也很多,但大多是个人项目,浅层封装,Turbo Vision 过去作为支持 Borland C++ 3.1 这种商用级 IDE 的界面库,不是这些玩票项目可以比得了的。

编程语言 TMBASIC 也是用 Turbo Vision 2 做的界面:

补充点历史,Turbo Vision 最初是 Borland 公司用 Pascal 实现的,用于 Turbo Pascal,后来在做 Borland C++ 3.x 的时候用 C++ 重新实现了一个版本,用于支持 BC31 的界面,在 BC31 里包含头文件和静态库,你可以直接用它构建 TUI 程序,接着 Borland 在 1997 年将 C++ 版本的完全开源了,就是我们上面说的这套的源头;

而 Pascal 版本的 Turbo Vision 一直没开源,Free Pascal 后面又根据 C++ 开源的版本重新实现了一套叫做 Free Vision 的界面库给 Free Pascal 做 IDE 用:

我很喜欢它的设置界面,根本不用读文档写配置文件,直接对话框里全部帮你归好类了:

c
是不是很眼熟?Free Pascal 的 IDE,背后使用的 Free Vision,和上面的 Turbo Vision 同出一源。

PS:Turbo Vision 还出过几本书:

想看的话到开头项目主页里有链接。

Loading

The post C++ 最好用的 TUI 界面库 Turbo Vision 2.0 appeared first on Skywind Inside.

如何清理你的系统盘(C: 盘)?

作者 skywind3000
2025年12月27日 00:32

就一年没清理 C 盘,今天清理出 140GB 的东西来,而且还没影响使用,所有程序都在往你的 C 盘拉屎。不要网友问我怎么清理下来的:

1)右键 C 盘属性,清理;
2)自己到 TEMP 目录手动清空,注意有三个 Temp:AppData 下面两个,Windows 目录下有一个;
3)SpaceSniffer 找占用大的目录,清空不合理的,这个需要一点经验,拿不准可问下 AI 某目录能不能清空;
4)清浏览器缓存;
5)最后 CCleaner 跑一遍。

上面一系列任务做完基本就清理出 100GB 的内容了,然后再使用 Dism++ 删除系统还原点后,又多出 40GB 来:

最开始剩余空间只有 130GB,现在有 272GB 剩余,可以做一次年度全盘备份了。

Loading

The post 如何清理你的系统盘(C: 盘)? appeared first on Skywind Inside.

Rust 适合开发游戏吗?

作者 skywind3000
2025年10月30日 13:48

Rust 并不适合开发游戏,它更擅长有明确定义,边界清晰的项目,这样你类型体操一遍做完问题不大,比如重构个已经十年没变过的 C 模块,但游戏领域并没有明确的定义,策划案不停修改,你的代码不但要求快速迭代还要求不停重构,那么类型体操在这过程中就变成锁死你的紧箍咒。

参考文章:3 年写了 10 万行代码开发者吐槽:当初用 Rust 是被忽悠了

这里解释一下,为什么当项目没有明确定义时,rust 为啥会显得笨拙?因为此时不但需求会随时间变来变去,更要命的是你没有一份十年没变过的 C 代码做参考,对项目整体实现缺乏全局的认识,只有自底向上的方法不断尝试和修正自己,不段反思和改进中上层代码,才能像盲人摸象那样逐步认清楚整个世界因该是啥样。

而此时类型体操会在这时勒得你喘不过气来,一个之前需要横着用的变量现在需要竖着用了,你思来想去发现你完全没办像其他语言那样改成法竖着用了,于是你只有引入更大范围的重写才能解决问题,你觉得这样很难受,去 rust 社区寻求帮助,但发现他们并不会帮助你真的解决语言问题,只会一个劲的指责你 “你觉得痛苦正是因为你对 rust 不熟悉导致的” 或者 “rust 逼迫你更大范围重写正是逼你尽早写出更好的代码”,他们这么说在定义清晰的项目里的确没问题,越早重构越好,但在定义不清晰的项目上里存在大量中间设计,你今天改成这样,八成不是最终形态,隔几天可能还要改,而此时 rust 却逼迫你每次都提前费精力进行更大范围的重写,即使这时完全没必要的,过两天就不需要的,他也不许你像其他语言那样用快速实现的方式应对新的中间状态,等需求稳定了,技术方案也收敛的情况下再进行迭代和完善,然后你就抓狂了!

网友 FENG DONG:项目没有明确定义时候,需要设计 cost function,然后让代码沿着 gradient 演进。但 Rust 不是一种可微的语言。

网友 Aaltonen:游戏开发是 “巨型可变状态球” (giant balls of mutable state),需求像 gradient descent 一样逐步演进,但 Rust 的借用检查器(borrow checker)和所有权模型强制你 “一次性想好”,导致小改动引发大重构

那么 Rust 适合开发游戏引擎么?我只能说 depends,主要到了现在也没啥 rust 开发游戏引擎的成功例子啊,一天到晚到处搞营销的 rust 游戏引擎 bevy 基本上类似个玩具,到现在都没啥靠谱的商业游戏案例,顶多几个独立小游戏之类,而且它要求用 rust 来写业务逻辑,这个上面已经论述过,它并不适合开发游戏业务逻辑;

其次游戏引擎有很多部件,有需要高度优化各种汇编技巧的性能部分(类似 ffmpeg),有需要同各种系统图形 api 打交道的抽象硬件层部分(不断同 direct3d,opengl,vulkan 还有各种系统 api 打交道),还有需要大量整合第三方(基本是 c++ 库,比如 imgui, box2d, sdl, bgfx, physx)的胶水层,这些也并不适合 rust。除非你觉得 ffmpeg 这样的项目适合用 rust 重写,或者可以用 rust 把大量久经考验的第三方库全部发明一遍。

最后,成熟的游戏引擎里都是有各种 hack 或者 dirty trick 的,rust 做这些并不是特别方便。

配图:“But it’s SAFE !!”

“But it's SAFE !!”

Loading

The post Rust 适合开发游戏吗? appeared first on Skywind Inside.

博客迁移服务商

作者 skywind3000
2025年10月24日 00:04

我的博客使用的 wordpress 一直架设在 bluehost.cn 上,差不多十多年了,前两天突然就不能访问了,后台也登录不进去,一查 bluehost.cn 主页,原来停止服务了,很多和我一样在上面运营十多年的网站都是说停就停,然后完全不提供数据备份的时间,连个电话都不给你打一个,就是邮件垃圾箱里提前几天给你发了一封告知邮件,人家 linode 之类的欠费停止你服务后,任然给你机会备份走,结果它完全不给。

所以没有啥完全稳定会一直存在的服务商,幸亏我机器上有备份,调查了以下几家虚拟主机提供商,他们的业务发展情况及稳定性,最后迁移 bluehost.com 了,帖子和评论都恢复了,但是页面访问统计没恢复到,因为页面访问统计插件是把数据存在别的 mysql 表里的,但之前用的备份插件又是只导出 wordpress 标准表格,所以包括访问统计,upvote 之类的数据全部清零了,无所谓了,还好访问量不算啥重要的东西,希望这次 bluehost.com 能多用两年。

之前我用过 bluehost.com 的,指示图 bluehost.cn 服务器在香港,国内访问会快点后面才选他,看来太小众的还是有问题。这次 bluehost.com 的服务器在亚利桑那,虽然远点,但国内访问起来,只要不下载,看个页面什么的也还算顺利。

有人问我为何不用静态页面?发布到 github pages 上那种,因为我这个 wordpress 博客以及个人 wiki 只依赖一家服务商,但是 github pages 页面需要依赖 github,评论需要依赖 gihtub issues+插件,计数器又需要依赖别的什么服务,依赖的服务太多了,上网那么多年我从不相信有什么服务可以一直持续下去的,所以依赖自然也少越好。

除了服务外,依赖的项目也是越少越好,之前那些 github pages 使用的基于 issues 的插件突然作妖,要收费,不收费就给你插广告,闹得天怒人怨,你还没办法,历史评论是博客相当重要的数据,依赖 github issues 作为评论数据存储的机制还有个致命方案是你还不能方便的导入导出,你会被绑死在 github issues 这个方案上,完全无法掌握自己的数据,不说 github 停止服务,一旦它一朝更改 api 规则(跟 twitter 一样)就只有哭了。

最后 wordpress 的功能真的很强大,插件生态也非常丰富,我也比较熟悉,有啥需求大概都能搞得定。

Loading

The post 博客迁移服务商 appeared first on Skywind Inside.

微信比 Telegram 落后在哪里?

作者 skywind3000
2025年9月30日 17:50

不怕不识货,就怕货比货,以下七点帮你了解差距究竟有多大:

第一:微信桌面 Qt 版引用了来自 Telegram 的 GPL 代码,对二进制 grep 一下字符串 desktop-app 就可以看到。有人找相关负责人提了一年了,结果连删掉字符串都懒得删。

第二:微信这么喜欢抄 Telegram 却不多抄点好的,比如微信群历史服务器只能保存两周,图片更短,当时没下载的图片几天后想看看就发现被服务器清除了,而 tg 全球十亿用户,消息存服务器四五年都不会删;

第三:什么年代了,微信附件大小最大 100M(不知改了没?)tg 随便存 10G 附件,一存好几年!一年前群里存的视频现在都找的到;

第四:微信那么挣钱的产品,结果给你搞个朋友圈合照都看不清脸的究极图片压缩,号称节省存储空间,好意思么?语音质量被压缩的经常听不清楚,飞书和钉钉的语音都比它清晰一万倍;

第五:微信用起来比 Telegram 卡有很多原因,其中一个重要原因就是微信使用单消息队列,所有消息挤压在一起,每次切换到微信就要同步一半天,包括全局消息和每个群最近的几百条消息,必须要同步完才能近一步操作,几秒时间消息列表不断更新跳变,点击无响应,导致想发给老婆的话明明点了老婆头像,最后因为列表跳变发到了同事群或者小孩班级群里去了,闹出了无穷无尽的笑话;但 Telegram 之所以不卡一个重要设计就是消息使用多队列,同时不需要等消息同步完才能操作,多个消息队列可以一边同步一边操作,每次切换过去你都可以立即流畅的操作,各个群的最近消息历史会使用并行的多个消息队列在后台并行同步,根本不影响你操作;

第六:微信非常耗电,有一次我手机只有 5% 的电了,希望能撑到出租车到家,所以我开启了节能模式,想着应该没事,正在结束着不同应用时来了条信息,我手贱条件反射的点了上去,瞬间就后悔了,熟悉的微信界面弹了出来,然后一个消息同步,手机被卡住不动,界面不停更新,手机瞬间变烫,几秒时间一下给我手机干没电自动关机了;

第七:微信技术团队比较喜欢吧屁大点事情说的玄乎其玄,不就是用了一个现成的叫 amrNB 的窄带语音编码器么,还只能编解码 8khz 的音频,明明有更好的做法,却用这种老旧淘汰的东西损失很多质量换来一点空间:

居然还做了三个通宵,还是他们当时广研最有技术含量的东西,这是在进一步自我暴露么?

所以真的不要和 Telegram 比,一比你发现全是差距。

Loading

The post 微信比 Telegram 落后在哪里? appeared first on Skywind Inside.

你是在什么电脑上学编程的?

作者 skywind3000
2025年5月24日 00:27

我小学时用的 GMT-92 学习机写程序,就是 FC 6502 那一套,但比后辈小霸王强多了:右上方有磁带盒,有很多扩展软件以磁带形式发布,自己写的 BASIC 程序也可以录制到磁带上下次接着写,小霸王那种没有存储设备一关机你的代码就没有的环境是根本没法学习编程的,因为代码完全无法积累,每次重来,你永远写不了复杂的软件,而这台 GMT-92 最核心的地方就是提供磁带存储,让我可以写稍微复杂点的代码,我就抱着一本 BASIC 说明书开始抄写上面的各种例子代码,然后修改部分不停验证学编程的:

那时我想做一个空战游戏,但是我只会每次循环控制一个飞机从左边移动到右边,完全不知道该如何同时控制多架飞机,因为身边没人教,没网络,书店里没游戏开发书籍,也没处问人,只能去书店看相关的电脑书籍得知有些操作系统是有多任务功能的,可以同时运行几段程序,我心想这就是我想要的,可 GMT 根本没有多任务这个功能怎么办呢?

连续数周没想出解决方案来,上课想,吃饭想,后来脑袋里面一遍遍的过以往学习过的电脑知识,让他们俩俩互相搭配,看看有没有新的可能性,突然我想起书店里介绍多任务原理的文字来,每个任务执行一小段时间片再去执行下一个任务的时间片,切换够快就能产生同时运行的效果,然后我忽然灵机一动,是不是我需要同时控制的多架飞机也可以这样每架飞机每次动一小步,然后快速切换下一架动一小步模拟出多架飞机同时移动的效果呢?

没有多任务系统,我能不能仿照实现呢?然后脑袋里一遍遍琢磨各处细节越想越觉得没问题,然后周末一整天,上午实现了一个基础,下午不停调试,居然被我做出来了,一个简单的空战游戏,多个敌人和主角可以正常同时移动,后来我才知道,这种每次更小一小步保存状态下次继续的方式就是经典的状态机模型,游戏做出来把我同学和身边小伙伴吓了一跳,也把我爹吓了一跳,他终于觉得或许我有潜力搞这个,才在后来初中时真的下决心给我买了台正儿八经的电脑,联想 486DX 66MHz,4MB 内存,DOS 系统,我才正儿八经的有条件开始自学 C 语言和汇编。

Loading

The post 你是在什么电脑上学编程的? appeared first on Skywind Inside.

Raylib 这种立即模式的图形引擎如何呢?

作者 skywind3000
2025年4月29日 21:11

最近 raylib 比较火,又是移植 web 又是移植掌机,看了眼 raylib 的例子代码,知道它为什么代码量那么少了,它甚至连一个显示对象树/场景树都没有实现,就是直接调用各种 draw 函数,和 ImGUI 一样属于 “立即模式”,这种模式做点简单的东西很舒服,对象一多一复杂就会比较麻烦:

那么这种每帧控制自己绘制的立即模式有啥问题呢?

第一:程序简单的时候好像也没区别,程序复杂了性能会急剧降低;

第二:正统图形引擎,都是维护一颗显示对象树/场景树,使用者将模型添加到书节点上,然后引擎能根据摄像机位置,高效剔除不需要显示的东西,raylib 这种立即模式相当于将大量工作,比如显示对象剔除,坐标变换等全部一古脑的推给用户,图形简单时可以不做剔除,但场景稍微复杂点不做剔除性能就会急剧下降;

第三:需要显示的对象多了以后,如果两帧之间大部分对象并没有改变,那么使用显示对象树可以基本不管,而立即模式下即便大量物体没改变也需要全部重新过一遍,大量数据重复传输到显存;

第四:对象多了以后,对脚本不友好,脚本调用 C 接口本来就缓慢,传统引擎脚本只需要维护两帧之间真正改变的对象,连动画这些自动的东西一旦开始都不用管,但立即模式下,所有对象每帧都需要用脚本操作一遍,开销巨大;

立即模式用来做 UI 问题不大,游戏界面本身没多复杂,但有序图形实用性就不太高了,所以 raylib 这样的立即模式图形引擎,更适合作为教学工具,而不是用于实际项目。

Loading

The post Raylib 这种立即模式的图形引擎如何呢? appeared first on Skywind Inside.

GCC 利用未定义行为进行优化正确么?

作者 skywind3000
2025年1月19日 00:37

说实话,编译器是否该利用 Undefined Behavior 进行优化目前都还是一个争议话题,主要是 gcc 开了个坏头,不予余力的在默认参数下利用 UB 来优化,举个例子,C 语言里带符号整数溢出是未定义行为,编译器应该假设它实际上以某种方式定义了:

int foo(unsigned char c) {
    int value = 2147483600;
    value += c;
    if (value < 2147483600) 
        bar();
    return value;
}

但利用这个 UB 进行优化的编译器会认为,既然 x 不会是负数,那么 value < 2147483600 就永远不会发生,所以整个 if 语句以及后面的 bar() 调用将可以被忽略,变成:

int foo(unsigned char c) {
    int value = 2147483600;
    value += c;
    return value;
}

这其实是一种很危险的做法,因为 C 语言可以跨越各种 CPU 架构编程,当年标准定义时,CPU 架构的差异比今天还大,在处理上面这类问题时,即便在今天,不同的架构结果并不一定相同,比如有的平台用补码表示负数,所以溢出了就会变成 -2147483648,而碰到反码或者原码表示负数的架构下,溢出了可能就变成 0 或者其它,所以一些事情根本就没法具体定义,必须留给具体编译器具体平台去处理。

Undefined Behavior 并不是说代码这么写是错的,相反他们都是语法正确的代码,真是错的应该就编译错误了,而是标准留个编译器实现者以自由,不去做限制,让他们根据实际平台,根据自己实现情况自行选择实现方式,而某些编译器实现选择利用它来进行优化了,那么如果本来就想利用特定平台的特性完成某些特定功能时,所以这类代码将没法写了。

关于这个问题,有个 X 友说的很准确,这里贴下译文(原文贴后面):

亲爱的 C 程序员们,既然你们似乎无法阅读关于你们编程语言的文档,让我为你们解释一下什么是未定义行为,更重要的是它不是什么。

引用 C 标准:

未定义行为:本国际标准未做任何强制要求的行为。(Undefined behavior: behavior for which this International Standard imposes no requirements)

这意味着标准并没有规定某些表达式该如何表现,编译器的创造者们可以自由选择他们想要的行为,例如:

  • 使编译失败
  • 删除有问题的表达式并继续编译
  • 发送给你的前任一条“我想你了”的消息
  • 将你的票投给一个 XX 主义政党
  • 甚至定义该行为(以平台特定的方式或非特定方式)

即,编译器的职责是合理处理未定义行为的情况。C 标准并没有对编译器作者提出挑战,这不是 “尽情发挥,给我惊喜”,它仅仅是对某些表达式没有施加限制,因为给予编译器作者自由选择其解决方案是合情合理的,因为这可能依赖于平台。

未定义行为并不是关于“被禁止的表达式”,它仅仅是语法上正确的 C 代码,而 C 标准对此并不关心。

到目前为止,这一切都是合理的。

在处理未定义行为时,编译器作者有多种选择:

  • 偏向一致的语义
  • 偏向性能
  • 偏向实现的简单性

正确的答案总是 “默认优先语义而非性能”。可以有破坏语义的优化,但这些优化应该通过适当的编译选项来启用,以便那些知道自己在做什么的人(或至少在继续破坏代码之前,编译器会得到明确的同意)使用。

(点击 more/continue 继续阅读)

让编译器默认具有一致的语义不仅减少了各种开发人员(包括经验丰富的开发人员)引入的错误,还极大地改善了开发人员的用户体验,因为这样可以让他们更好地推理代码,毫无畏惧地进行重构,并且总体上不必时刻担心优化器会搞砸他们的代码。

至于 “但您必须支持多个平台” 的论点,如果 GCC 的开发者想要这样,他们就会让整数溢出在编译时的行为与运行时的行为相同。也就是说,如果在运行时是环绕运算,那么在编译时也是环绕运算。或者,他们可以简单地拒绝承诺任何特定的整数溢出策略,而只是不过度尝试优化涉及算术运算的表达式。

不幸的是,GCC 的开发者和他们的社区一样愚蠢,他们不仅没有合理地定义未定义行为,反而假装这一切都与禁止表达式有关,并默认启用他们那些让语义破裂的优化。“看,妈的,代码快了5%,却带来了 5% 的更多 bug,搞得全世界的基础设施和生产代码都乱了!”

至于他们在整数溢出问题上的选择有多愚蠢,我们会另行讨论,但这则推文的要点并不是某个具体优化的糟糕,而是 GCC 将 C标准中的未定义行为作为借口来搞砸自己编译器的整体方法,这真是让人难以置信——这是只有少数人才能理解的全面退化。

任何有不同看法的人都应该被视为愚蠢,应该被限制接触任何计算机,保持50米的安全距离。

原文:https://x.com/effectfully/status/1876231418357092534

这里还有个 原文长截图

Loading

The post GCC 利用未定义行为进行优化正确么? appeared first on Skywind Inside.

异步事件模型的 Self-pipe trick

作者 skywind3000
2024年11月4日 11:41

异步事件模型中有一个重要问题是,当你的 select/poll 循环陷入等待时,没有办法被另外一个线程被唤醒,这导致了一系列问题:

1)在没有 pselect/ppoll 的系统上,信号无法中断 select/poll 等待,得不到即时处理;
2)另一个线程投递过来的消息,由于 select/poll 等待,无法得到即时处理;
3)调短 select/poll 的超时时间也无济于事,poll 的超时精度最低 1ms,粗糙的程序可能影响不大,但精细的程序却很难接受这个超时;
4)有的系统上即便你传了 1ms 进去,可能会等待出 15ms 也很正常。

比如主线程告诉网络线程要发送一个数据,网络线程还在 select/poll 那里空等待,根本没有机会知道自己自己的消息队列里来了新消息;或者多个 select/poll 循环放在不同线程里,当一个 accept 了一个新连接想转移给另一个时,没有办法通知另一个醒来即时处理。

解决这个问题的方法就叫做 self-pipe trick,顾名思义,就是创建一个匿名管道,或者 socketpair,把它加入 select/poll 中,然后另外一个线程想要唤醒它的话,就是往这个管道或者 socketpair 里写一个字节就行了。

类似 java 的 nio 里的 selector 里面的 notify() 函数,允许其他线程调用这个函数来唤醒等待中的一个 selector。

具体实现有几点要注意,首先是使用 notify() 唤醒,不用每次调用 notify() 都往管道/socketpair 里写一个字节,可以加锁检测,没写过才写,写过就不用写了:

// notify select/poll to wake up
void poller_notify(CPoller *poller) {
    IMUTEX_LOCK(&poller->lock_pipe);
    if (poller->pipe_written == 0) {
        char dummy = 1;
        int hr = 0;
    #ifdef __unix
        hr = write(poller->pipe_writer_fd, &dummy, 1);
    #else
        hr = send(poller->pipe_writer_fd, &dummy, 1);
    #endif
        if (hr == 1) {
            poller->pipe_written = 1;
        }
    }
    IMUTEX_UNLOCK(&poller->lock_pipe);
}

大概类似这样,在非 Windows 下面把 pipe() 创建的两个管道中的其中一个放到 select/poll 中,所以用 write(),而 Windows 下的 select 不支持放入管道,只支持套接字,所以把两个相互连接的套接字里其中一个放入 select。

两个配对的管道命名为 reader/writer,加入 select 的是 reader,而唤醒时是向 writer 写一个字节,并且判断,如果写过就不再写了,避免不停 notify 导致管道爆掉,阻塞线程。

而作为网络线程的 select/poll 等待,每次被唤醒时,甭管有没有网络数据,都去做一次管道复位:

static void poller_pipe_reset(CPoller *poller) {
    IMUTEX_LOCK(&poller->lock_pipe);
    if (poller->pipe_written != 0) {
        char dummy = 0;
        int hr;
    #if __unix
        hr = read(poller->pipe_reader_fd, &dummy, 1);
    #else
        hr = recv(poller->pipe_reader_fd, &dummy, 1);
    #endif
        if (hr == 1) {
            poller->pipe_written = 0
        }
    }
    IMUTEX_UNLOCK(&poller->lock_pipe);
}

每次 select/poll 醒来,都调用一下这个 poller_pipe_reset(),这样确保管道里的数据被清空后,就可以复位 pipe_written 标志了。

让后紧接着,处理完所有网络事件,就检查自己内部应用层的消息队列是否有其他消息投递过来,再去处理这些事件去;而其他线程想给这个线程发消息,也很简单,消息队列里塞一条,然后调用一下 notify(),把该线程唤醒,让他可以马上去检查自己的消息队列。

主循环大概这样:

while (is_running) {
    // 1)调用 select/poll 等待网络事件,超时设置成 1ms-10ms;
    // 2)醒来后先处理所有网络事件;
    // 3)如果和上次等待之间超过 1毫秒,则马上处理所有时钟超时事件;
    // 4)检查自己消息队列,并处理新到来的事件。
}

差不多就是这样。

PS:有人说用 eventfd 也能实现类似效果,没错,但不能跨平台,只有 Linux 特有,而且还有一些坑,但 self-pipe trick 是跨平台的通用解决方案,不管你用 Windows / FreeBSD / Linux / Solaris 都可以使用这个功能。

Loading

The post 异步事件模型的 Self-pipe trick appeared first on Skywind Inside.

WinSock 可以把 SOCKET 类型转换成 int 保存么?

作者 skywind3000
2024年11月1日 17:04

在 Linux/Unix 等 posix 环境中,每个套接字都是一个文件描述符 fd,类型是 int,使用起来非常方便;但在 Win32 环境中是 SOCKET 类型被定义成 UINT_PTR ,是一个指针,在 x64 环境中一个 SOCKET 占用 8 个字节。

那么是否能将 SOCKET 类型强制转换成 int 类型保存没?这样就能统一用 int 在所有平台下表示套接字了,同时在 x64 环境下这样将 64 位的指针转换为 32 位的整数是否安全?

答案是可以的,下面将从三个方面说明一下。

Kernel Object

每个 SOCKET 背后其实都是一个指向 Kernel Object 的 Handle,而每个进程的 Handle 的数量是有限的,见 MSDN 的 Kernel Objects

Kernel object handles are process specific. That is, a process must either create the object or open an existing object to obtain a kernel object handle. The per-process limit on kernel handles is 2^24. However, handles are stored in the paged pool, so the actual number of handles you can create is based on available memory.

单进程不会超过 2^24 个,每个 Kernel Object 需要通过一个 Handle 来访问:

这些 Handle 保存于每个进程内位于低端地址空间的 Handle Table 表格,而这个 Handle Table 是连续的,见 MSDN 中的 Handles and objects

Each handle has an entry in an internally maintained table. Those entries contain the addresses of the resources, and the means to identify the resource type.

这个 Handle Table 表格对用户进程只读,对内核是可读写,在进程结束时,操作系统会扫描整个表格,给每个有效 Handle 背后指向的 Kernel Object 解引用,来做资源回收。

所以看似是 UINT_PTR 指针的 SOCKET 类型,其实也只是一个表格索引而已,这个 Handle Table 表格的项目有数量限的(最多 2^24 个元素),内容又是连续的,那当然可以用 int 来保存。

开源案例

故此不少开源项目也会选择在 Windows 环境下将 SOCKET 类型直接用 int 来存储,比如著名的 openssl 在 include/internal/sockets.h 里有解释:

/*
 * Even though sizeof(SOCKET) is 8, it's safe to cast it to int, because
 * the value constitutes an index in per-process table of limited size
 * and not a real pointer. And we also depend on fact that all processors
 * Windows run on happen to be two's-complement, which allows to
 * interchange INVALID_SOCKET and -1.
 */
#   define socket(d,t,p)   ((int)socket(d,t,p))
#   define accept(s,f,l)   ((int)accept(s,f,l))

所以 openssl 不论什么平台,都将套接字看作 int 来使用:

int SSL_set_fd(SSL *ssl, int fd);
int SSL_set_rfd(SSL *ssl, int fd);
int SSL_set_wfd(SSL *ssl, int fd);

所以它的这些 API 设计,清一色的 int 类型。

程序验证

道理前面都讲完了,下面写个程序验证一下:

void create(int n) {
    std::vector<SOCKET> sockets;
    for (int i = 0; i < n; i++) {
        SOCKET s = socket(AF_INET, SOCK_STREAM, 0);
        if (s == INVALID_SOCKET) {
            printf("socket failed with error %d\n", WSAGetLastError());
            break;
        }
        sockets.push_back(s);
        printf("index=%d socket=%llu\n", i, (uint64_t)s);
    }
    for (int i = 0; i < n; i++) 
        closesocket(sockets[i]);
    printf("\n");
}

int main(void) {
    WSADATA WSAData;
    WSAStartup(0x202, &WSAData);

    printf("Round 1:\n");
    create(10);

    printf("Round 2:\n");
    create(10);
    return 0;
}

在 64 位环境下,创建 10 个套接字,然后释放,再创建 10 个:

Round 1:
index=0 socket=352
index=1 socket=324
index=2 socket=340
index=3 socket=332
index=4 socket=336
index=5 socket=356
index=6 socket=360
index=7 socket=364
index=8 socket=368
index=9 socket=372

Round 2:
index=0 socket=372
index=1 socket=368
index=2 socket=364
index=3 socket=360
index=4 socket=376
index=5 socket=356
index=6 socket=336
index=7 socket=332
index=8 socket=340
index=9 socket=324

可以看出,即便在 64 位下面:1)SOCKET 指向的表格项目是连续的;2)前面释放掉的表格项目,是会被后面复用的;3)他们都在表格范围内,不会由于不停创建/销毁导致 SOCKET 的数值持续增长。

成功的印证了前面关于 Kernel Object 和 Handle Table 的解释。


补充:刚才我又写了个程序,不停创建 socket ,看能创建多少个,以及 socket 的值最高去到多少:

最终结果(64 位程序,台式机内存 96GB):

  • 总共可以创建 16711629 个,刚好是 0xfeffcd 差不多就是 2^24 那么多。
  • SOCKET 的值去到 67108860 = 0x3fffffc,差不多是 2^26 的大小。
  • 当创建到 1611629 个以后,发生 10055 错误退出。

差不多就这样。

Loading

The post WinSock 可以把 SOCKET 类型转换成 int 保存么? appeared first on Skywind Inside.

WinSock 的 select 如何超过 64 个套接字限制?(三种方法)

作者 skywind3000
2024年10月31日 23:31

在做跨平台网络编程时,Windows 下面能够对应 epoll/kevent 这类 reactor 事件模型的 API 只有一个 select,但是却有数量限制,一次传入 select 的 socket 数量不能超过 FD_SETSIZE 个,而这个值是 64。

所以 java 里的 nio 的 select 在 Windows 也有同样的数量限制,很多移植 Windows 的服务程序,用了 reactor 模型的大多有这样一个限制,让人觉得 Windows 下的服务程序性能很弱。

那么这个数量限制对开发一个高并发的服务器显然是不够的,我们是否有办法突破这个限制呢?而 cygwin 这类用 Win32 API 模拟 posix API 的系统,又是如何模拟不受限制的 poll 调用呢?

当然可以,大概有三个方法让你绕过 64 个套接字的限制。

方法1:重定义 FD_SETSIZE

首先可以看 MSDN 中 winsock2 的 select 帮助,这个 FD_SETSIZE 是可以自定义的:

Four macros are defined in the header file Winsock2.h for manipulating and checking the descriptor sets. The variable FD_SETSIZE determines the maximum number of descriptors in a set. (The default value of FD_SETSIZE is 64, which can be modified by defining FD_SETSIZE to another value before including Winsock2.h.)

而在 winsock2.h 中,可以看到这个值也是允许预先定义的:

#ifndef FD_SETSIZE
#define FD_SETSIZE 64
#endif

只要你在 include 这个 winsock2.h 之前,自定义了 FD_SETSIZE,即可突破 64 的限制,比如在 cygwin 的 poll 实现 poll.cc,开头就重定义了 FD_SETSIZE

#define FD_SETSIZE 16384        // lots of fds
#include "winsup.h"
#include <sys/poll.h>
#include <sys/param.h>

定义到了一个非常大的 16384,最多 16K 个套接字一起 select,然后 cygwin 后面继续用 select 来实现 posix 中 poll 函数的模拟。

这个方法问题不大,但有两个限制,第一是到底该定义多大的 FD_SETSIZE 呢?定义大了废内存,每次 select 临时分配又一地内存碎片,定义少了又不够用;其次是程序不够 portable,头文件哪天忘记了换下顺序,或者代码拷贝到其它地方就没法运行。

因此我们有了更通用的方法2。

(点击 more/continue 继续)

方法2:自定义 fd_set 结构体

这个方法更为通用,按照 MSDN 里的 fd_set 定义:

typedef struct fd_set {
  u_int  fd_count;
  SOCKET fd_array[FD_SETSIZE];
} fd_set, FD_SET, *PFD_SET, *LPFD_SET;

结构体里第一个成员 fd_count 代表这个 fd_set 里总共保存了多少个套接字,而后面的 fd_array 数组则存储了各个套接字的值,它的大小由前面的 FD_SETSIZE 宏决定,这决定了最大可存储数量。

我们来看看 winsock2.h 中几个操作 fd_set 的宏的实现:

#ifndef FD_ZERO
#define FD_ZERO(set) (((fd_set *)(set))->fd_count=0)
#endif

清空操作很简单,就是把 fd_count 设置成零就行了,而增加一个套接字:

#define FD_SET(fd, set) do { u_int __i;\
    for (__i = 0; __i < ((fd_set *)(set))->fd_count ; __i++) {\
        if (((fd_set *)(set))->fd_array[__i] == (fd)) {\
            break;\
        }\
    }\
    if (__i == ((fd_set *)(set))->fd_count) {\
        if (((fd_set *)(set))->fd_count < FD_SETSIZE) {\
            ((fd_set *)(set))->fd_array[__i] = (fd);\
            ((fd_set *)(set))->fd_count++;\
        }\
    }\
    } while(0)

简单来讲就是先判断数组里是否已经包含,如果没包含并且 fd_count 小于 FD_SETSIZE 的话就追加到 fd_array 后面并且增加 fd_count 值。

那么方案就是用一个动态结构模拟这个 fd_set 就行了,要用时直接强制类型转换成 fd_set 指针传递给 select 即可,微软的 devblogs 里一篇文章讲过这个方法:

但是它的实现是用模板做了个新的 fd_set 结构体,一旦实例化就定死了,我给一个更好的跨平台实现:

#define ISOCK_ERECV     1       /* event - recv           */
#define ISOCK_ESEND     2       /* event - send           */
#define ISOCK_ERROR     4       /* event - error          */

/* iselect: fds(fd set), events(mask), revents(received events) */
int iselect(const int *fds, const int *events, int *revents, 
    int count, long millisec, void *workmem);

这个函数第一个参数 fds 传入 fd 数组,然后 events 传入对应 fd 需要捕获的事件,相当于 poll 里的 events,而 revents 用于接受返回的事件,最后 count 代表总共有多少个 fd,前面的参数模仿了 poll 函数只是没用 struct pollfd 这个结构体表达而已。

最后一个参数 workmem 代表需要用多少内存,如果为 NULL 的话,这个函数不会调用下层的 select/poll 而会根据 count 数量计算出需要用到的内存并返回给你,让你安排好内存,第二次调用时用 workmem 传入内存指针:

int my_select1(const int *fds, const int *event, int *revent, int count, long millisec) {
    int require = iselect(NULL, NULL, NULL, count, 0, NULL);
    if (require > current_buffer_size) {
        current_buffer = realloc(current_buffer, require);
        current_buffer_size = require;
    }
    return iselect(fds, event, revent, count, millisec, current_buffer);
}

这样用就行了,这个 current_buffer 可以是一个全局变量,也可以放在你封装的 selector/poller 对象里。

或者栈上开辟一块空间,如果少量 select 就用栈空间,否则临时分配:

int my_select2(const int *fds, const int *event, int *revent, int count, long millisec) {
    #define MAX_BUFFER_SIZE 2048
    char stack[MAX_BUFFER_SIZE];
    char *buffer = stack;
    int require = iselect(NULL, NULL, NULL, count, 0, NULL);
    if (require > MAX_BUFFER_SIZE) buffer = (char*)malloc(require);
    int hr = iselect(fds, event, revent, count, millisec, buffer);
    if (buffer != stack) free(buffer);
    return hr;
}

这样可以避免维护一个全局变量。

下面给出 iselect 这个函数的实现,它能完全模拟 poll 的行为,突破 FD_SETSIZE 的限制,并且在非 Windows 下用 poll 而 Windows 下用 select:

/* iselect: fds(fd set), events(mask), revents(received events) */
int iselect(const int *fds, const int *events, int *revents, int count, 
    long millisec, void *workmem)
{
    int retval = 0;
    int i;

    if (workmem == NULL) {
        #if defined(__unix) || defined(__linux)
        return count * sizeof(struct pollfd);
        #else
        size_t unit = 32;
        size_t size = count * sizeof(SOCKET) + unit + 8;
        return (int)(size * 3);
        #endif
    }    
    else {
        #if defined(__unix) || defined(__linux)
        struct pollfd *pfds = (struct pollfd*)workmem;

        for (i = 0; i < count; i++) {
            pfds[i].fd = fds[i];
            pfds[i].events = 0;
            pfds[i].revents = 0;
            if (events[i] & ISOCK_ERECV) pfds[i].events |= POLLIN;
            if (events[i] & ISOCK_ESEND) pfds[i].events |= POLLOUT;
            if (events[i] & ISOCK_ERROR) pfds[i].events |= POLLERR;
        }

        poll(pfds, count, millisec);

        for (i = 0; i < count; i++) {
            int event = events[i];
            int pevent = pfds[i].revents;
            int revent = 0;
            if ((event & ISOCK_ERECV) && (pevent & POLLIN)) 
                revent |= ISOCK_ERECV;
            if ((event & ISOCK_ESEND) && (pevent & POLLOUT))
                revent |= ISOCK_ESEND;
            if ((event & ISOCK_ERROR) && (pevent & POLLERR))
                revent |= ISOCK_ERROR;
            revents[i] = revent & event;
            if (revents[i]) retval++;
        }

        #else
        struct timeval tmx = { 0, 0 };
        size_t unit = 32;
        size_t size = count * sizeof(SOCKET) + unit + 8;
        FD_SET *fdr = (FD_SET*)(((char*)workmem) + 0);
        FD_SET *fdw = (FD_SET*)(((char*)workmem) + size);
        FD_SET *fde = (FD_SET*)(((char*)workmem) + size * 2);
        void *dr, *dw, *de;
        int maxfd = 0;
        int j;

        fdr->fd_count = fdw->fd_count = fde->fd_count = 0;

        for (i = 0; i < count; i++) {
            int event = events[i];
            int fd = fds[i];
            if (event & ISOCK_ERECV) fdr->fd_array[(fdr->fd_count)++] = fd;
            if (event & ISOCK_ESEND) fdw->fd_array[(fdw->fd_count)++] = fd;
            if (event & ISOCK_ERROR) fde->fd_array[(fde->fd_count)++] = fd;
            if (fd > maxfd) maxfd = fd;
        }

        dr = fdr->fd_count? fdr : NULL;
        dw = fdw->fd_count? fdw : NULL;
        de = fde->fd_count? fde : NULL;

        tmx.tv_sec = millisec / 1000;
        tmx.tv_usec = (millisec % 1000) * 1000;

        select(maxfd + 1, (fd_set*)dr, (fd_set*)dw, (fd_set*)de, 
            (millisec >= 0)? &tmx : 0);

        for (i = 0; i < count; i++) {
            int event = events[i];
            int fd = fds[i];
            int revent = 0;
            if (event & ISOCK_ERECV) {
                for (j = 0; j < (int)fdr->fd_count; j++) {
                    if (fdr->fd_array[j] == (SOCKET)fd) { 
                        revent |= ISOCK_ERECV; 
                        break; 
                    }
                }
            }
            if (event & ISOCK_ESEND) {
                for (j = 0; j < (int)fdw->fd_count; j++) {
                    if (fdw->fd_array[j] == (SOCKET)fd) { 
                        revent |= ISOCK_ESEND; 
                        break; 
                    }
                }
            }
            if (event & ISOCK_ERROR) {
                for (j = 0; j < (int)fde->fd_count; j++) {
                    if (fde->fd_array[j] == (SOCKET)fd) { 
                        revent |= ISOCK_ERROR; 
                        break; 
                    }
                }
            }
            revents[i] = revent & event;
            if (revent) retval++;
        }
        #endif
    }

    return retval;
}

这就是我目前用的方法,刚好一百多行,这个方法我测试过,在我的台式机上同时维护一万个 socket 连接问题不大,做 echo server,每个连接每秒一条消息往返,只是 CPU 占用回到 70% 左右。

对于 Windows 下的客户端程序,维护的连接不多,这个函数足够用;而对于服务端程序,则可以做到能跑,可以让你平时跑在 Linux 下的服务端程序保证能在 Windows 下正常工作,正常开发调试,不论连接有多少。

这个方法唯一问题是 CPU 占用过高,那么 Windows 下面是否有像 kevent/epoll 一样丝滑的异步事件模型,既能轻松 hold 上万的套接字,又不费 CPU 呢?当然有,但是在说方案三之前先说两个错误的例子。

错误的选择:WSAEventSelect 和 WSAAsyncSelect

不少人提过函数 WSAEventSelect,它可以把套接字事件绑定到一个 WSAEVENT 上面:

int WSAAPI WSAEventSelect(
  [in] SOCKET   s,
  [in] WSAEVENT hEventObject,
  [in] long     lNetworkEvents
);

这个 WSAEVENT 是一个类似 EVENT 的东西,看起来好像没有 FD_SETSIZE 的个数限制,但问题 WSAWaitForMultipleEvents 里你同样面临 WSA_MAXIMUM_WAIT_EVENTS 的限制,在 winsock2.h 里:

#define WSA_MAXIMUM_WAIT_EVENTS (MAXIMUM_WAIT_OBJECTS)

后面这个 MAXIMUM_WAIT_OBJECTS 的数量就是 64,你还是跳不开。另外一个函数 WSAAsyncSelect 可以把 socket 事件关联到窗口句柄上:

int WSAAsyncSelect(
  [in] SOCKET s,
  [in] HWND   hWnd,
  [in] u_int  wMsg,
  [in] long   lEvent
);

这的确没有个数限制了,问题是你需要一个窗口句柄 HWND,你需要创建一个虚拟窗口,那么为了模拟 posix 的 poll 行为,你打算把这个虚拟窗口放哪里呢?它的消息循环需要一个独立的线程来跑么?

Unix 的哲学是一切皆文件,Windows 的哲学是一切皆窗口,没想到有一天写网络程序也要同窗口打交道了吧?总之也是个不太干净的做法。

方法3:用 iocp 实现 epoll

是的可以用 iocp 完全模拟实现 epoll,让你拥有一个高性能的 reactor 事件模型,轻松处理 10w 级别的套接字,听起来很诱惑但是很难实现,没关系,有人帮你做了:

这个 wepoll 的项目意在使用 iocp 实现 Windows 下的高性能 epoll,支持 vista 以后的系统,并且只有两个文件 wepoll.hwepoll.c,十分方便集成,接口也是对应 epoll 的:

HANDLE epoll_create(int size);
HANDLE epoll_create1(int flags);

int epoll_close(HANDLE ephnd);

int epoll_ctl(HANDLE ephnd,
              int op,
              SOCKET sock,
              struct epoll_event* event);

int epoll_wait(HANDLE ephnd,
               struct epoll_event* events,
               int maxevents,
               int timeout);

完全跟 epoll 一样用就完事了,不过只支持 Level-triggered 不支持 Edge-triggered,不过有性能测试表明 Edge-triggered 并没有太大优势,且并不跨平台,其它平台的异步事件 API 大多也不兼容这个模式,所以用 Level-triggered 问题不大。

PS:libevent 新版本在 Windows 下就是用的这个 wepoll,应该是经过考验的项目了。

话题总结

那么假设你在做一个跨平台的 poll 模块,在 Windows 下上面的三套方案用哪套好呢?我的做法是内部实现的是第二套方案自定义 fd_set,它可以兼容到 Windows 95,算是个保底的做法,同时提供插件机制,可以由外部实现来进行增强。

然后主程序检测到系统在 vista 以后,并且包含了 wepoll 的时候,把 wepoll 的实现做一层插件封装安装进去。

Loading

The post WinSock 的 select 如何超过 64 个套接字限制?(三种方法) appeared first on Skywind Inside.

DOS 经典软件下载

作者 skywind3000
2024年10月16日 15:55

二十多年前的某一天,我盯着资源管理器里很久没用却一直舍不得删除的 UCDOS 文件夹犹豫了半天,最终却为了给硬盘腾点空间一狠心 shift+delete 把他们彻底删除了,当时我没意识到,一个时代就这样彻底的离我远去;二十多年后的今天,我又在最新版的 DOSBOX 里把这些当年的工具一个个重新装了回去,软件没变,但是消逝的青春却再也回不来了。

做了一个《上古软件仓》,包含上古时代的编程工具,汉字系统和设计软件等,都是一些我以前经常用的软件,主打怀旧和娱乐。

截图:中文系统

(点击 more/continue 继续)

截图:WPS

截图:整人专家 2000

其它工具包括:

CCED,Borland C++,Watcom C++,Turbo C,QBasic,FoxBase,sea 1.3 等。

欢迎访问:

https://skywind.me/wiki/%E4%B8%8A%E5%8F%A4%E8%BD%AF%E4%BB%B6%E4%BB%93

Loading

The post DOS 经典软件下载 appeared first on Skywind Inside.

Vim 文本过滤/文字处理插件

作者 skywind3000
2024年10月13日 23:30

我经常有文本处理的需求,例如将 html 转换成纯文本,或者移除 markdown 里的所有连接,或者繁体转换简体。因此我做了一个插件来管理和执行各种外部文本过滤器。

所谓 “文本过滤器” 是一个命令行程序,它从标准输入读取文本,然后进行一些处理后写到标准输出,在 Vim 里可以用原生的 :{range}! xxx 命令将选中文本发送给 xxx 命令的标准输入,然后用该命令的标准输出替换选中文本,这个命令很有用,但每次输入一长串命令略显繁琐,并且过滤器多了以后也很难管理。

因此我做了这个插件来统一管理文本过滤程序,并且提供接口来执行他们:

比如上图演示了将 HTML 转换成文本,以及去除 markdown 中的连接,使用命令 :{range}TP {name} 就能调用名为 {name} 的文本过滤程序了。这些程序可以用你喜欢的语言编写,放到统一的目录,加上可执行属性就行,该插件就能找到它。

而你在调试你的文本过滤脚本时,可以加个叹号 :{range}TP! {name} 这样你就可以在另一个窗口里预览结果,而不会覆盖到原文本:

这样调试起来比较方便反复运行。

编写一个文本过滤脚本也很简单,比如在 ~/.vim/text 目录内创建 “markdown_to_dokuwiki.sh” 文件:

#! /usr/bin/bash
pandoc -f markdown-simple_tables-multiline_tables+pipe_tables -t dokuwiki

就可以用 :%TP markdown_to_dokuwiki 命令将当前窗口内的 markdown 转换成 dokuwiki 语法。

插件地址:

https://github.com/skywind3000/vim-text-process

今天我数了一下,不知不觉我已经开发了 20 个 Vim 插件了:

https://skywind.me/wiki/vim_plugins

欢迎尝试。

Loading

The post Vim 文本过滤/文字处理插件 appeared first on Skywind Inside.

Python 的 asyncio 网络性能比 C 写的 Redis 还好?

作者 skywind3000
2024年10月10日 23:15

先前我做过一个 asyncio/gevent 的性能比较《性能测试:asyncio vs gevent vs native epoll》,今天修改了一下 asyncio 的测试程序的消息解析部分,改用 Protocol,发现它甚至比 redis 还快了:

安装依赖:

pip install hiredis uvloop

编辑 echosvr.py 文件:

import asyncio
import hiredis

d = {}

def process(req):
    cmd = req[0].lower()
    if cmd == b'set':
        d[req[1]] = req[2]
        return b"+OK\r\n"
    elif cmd == b'get':
        v = d.get(req[1])
        if v is None:
            return b'$-1\r\n'
        else:
            return b'$1\r\n1\r\n'
    elif cmd == b'config':
        return b'-ERROR\r\n'
    else:
        return b'-ERROR\r\n'
    return b''

class RedisServerProtocol(asyncio.Protocol):
    def connection_made(self, transport):
        self.transport = transport
        self.hireader = hiredis.Reader()

    def data_received(self, data):
        self.hireader.feed(data)
        while True:
            req = self.hireader.gets()
            if not req:
                break
            res = process(req)
            self.transport.write(res)

async def main():
    loop = asyncio.get_running_loop()
    server = await loop.create_server(lambda: RedisServerProtocol(), '0.0.0.0', 5003)
    print('serving on {}'.format(server.sockets[0].getsockname()))
    async with server:
        await server.serve_forever()

try:
    import uvloop
    uvloop.install()
except ImportError:
    print('uvloop is not available')

asyncio.run(main())

启动:

python echosvr.py

测试:

redis-benchmark -p 5003 -t get -n 100000 -r 100000000
...
95238.10 requests per second

同一台机器上测试 redis:

redis-benchmark -p 6379 -t get -n 100000 -r 100000000
...
91407.68 requests per second

同一台机器上,Python 版本的服务可以跑到 95k requests/s 而 redis 只能跑到 91k requests/s,前者比后明显快了 5%。

那么 asyncio 比 C 写的 redis 性能还好么?其实不是,因为 redis 做了更多事情,比如为了更高并发,它把读写线程从主线程分离出去了,而这个 Python 代码只有一个高度紧凑的主线程;其次 redis 功能复杂,结构也相应有更多层次的抽线,一条消息从接收到处理需要经过多个模块,这是业务复杂度上到一定层次必然的。

目前市面上不少新项目号称比久经考验的老项目更快,其中部分也就是和这个 python 测试程序类似,老项目考虑的东西它们都不考虑,就是浅浅一层,用最简单的测试跑一下,好像是比老项目快那么一点了,但是复杂度但凡稍微上去一点,只需要它实现老项目一半不到的功能,那点薄薄的优势立马荡然无存,马上跑的比老项目还慢。

Loading

The post Python 的 asyncio 网络性能比 C 写的 Redis 还好? appeared first on Skywind Inside.

EditPlus 的配置方法

作者 skywind3000
2024年9月5日 23:05

作为一名编辑器爱好者,EditPlus 是我最喜欢的编辑器之一,超过 NotePad++,它启动速度比它快,打开文件比它快,功能比它强,颜值也比它高,但大小只有 2MB:

用了这么多年,我感觉我欠 EditPlus 一篇文章,介绍一下我平时是如何是用 EditPlus 搭建开发环境的,以及如何让它变得更好用:

(点击 more/continue 展开)

先配置下 GCC 的工具,选择主菜单 Tools-> Configure User Tools:

然后点右边按钮 “Add Tool” 并选择 “Program”,然后在下面填空:

Menu text: Execute Program
Command: C:\Windows\system32\cmd.exe
Argument: /C "$(FileNameNoExt)"
Initial directory: $(FileDir)

这些形如 $(FileDir) 之类的宏不用死记,点输入框右边的下箭头按钮,就能弹出菜单,让你选择后自动插入:

配置好以后,点下方 “Apply” 按钮确定,这样我们多出一个工具来,按 Ctrl+1 运行当前 C++ 程序;继续添加一个编译工具,接着点击 “Add Tool” 选择 “Program”,填写如下内容:

具体内容:

Menu text: GCC Compile
Command: D:\Dev\mingw\bin\gcc.exe
Argument: "$(FileName)" -o "$(FileNameNoExt)"
Initial directory: $(FileDir)

注意文件名用引号括号,防止带空格的文件名出错,其次下方的 “Action” 选择 “Capture output”,这样就不会弹出新的 cmd 窗口运行,而是将输出捕获到下方面板,并且匹配输入错误,设置好按 OK。

这样在主目录的 Tools 菜单下面,就出现了两个新配置的用户工具,分别按 Ctrl+1 和 Ctrl+2 运行,比如我们先按 Ctrl+2:

编译当前文件,因为没啥输出,所以下面的面板也没啥内容,注意有的 MinGW 环境可能需要你把他的 bin 目录设置到 PATH 环境变量中,你可以设置个全局的,编译成功后再按 Ctrl+1 运行程序:

运行程序因为没有捕获输出,所以是弹出终端窗口运行的,而且运行完会提醒你按任意键继续,一般编译器调用这种有错误输出的,都是配置成捕获输出,让错误在下面显示,比如故意把程序改错再编译一次:

下面显示了错误,并且支持鼠标双击就跳转到具体位置,这样一套简易的 C++ 开发环境搭建成功了,照葫芦画瓢,再配置一个 Python 运行工具:

这样按 Ctrl+3 就可以运行当前 Python 脚本,类似的方法,还可以将 GNU Make / CMake 之类的工具配置进去,无外乎把 $(FileDir) 换成 $(ProjectDir) 代表项目目录,或者 $(DirWin) 代表左边文件树的根目录。

配置好工具以后,在配置一些快捷键,继续在这个配置窗口选择左边 Tools 下面的 Keyboard 选项:

右边 Types 列表选择 Window,然后 Commands 选择 Window1,然后在下面 Press new shortcut 的地方点下鼠标,然后按 ALT+1,再点右边的 Assign 按钮:

这样就可以用 ALT+1 时切换到第一个标签页的文件,同理把 Window2 到 Window9 赋给 ALT+2 到 ALT+9,以及 Windows10 赋给 ALT+0,这样当同时打开多个文件的时候 ALT+Num 进行快速切换。

不过文件多的时候也可以用主菜单 Window -> Window List (Shift+F12) 来显示窗口列表,供你快速切换:

并且提供模糊搜索功能,敲入部分文件名就能快速筛选,类似 VS 里的 VAX 红番茄插件的 Alt+shift+o 功能。

最后调整下支持的语言,还是 Tools -> Preference 窗口,左边分类那里选择 File 下面的 Settings & Syntax 然后中间文件类型选择 C/C++:

然后把下面 Auto Completion 前面的勾去了,如果不喜欢它奇怪的补全系统的话,进一步的话,新增一些语言,到 EditPlus 官网的 User Files 上去下载一些语法文件设置上去,比如 cmake 之类的,增加下新的语法高亮。

基本设置差不多就这些,这样应该比之前好用不少了。

技巧1:运行时自动检测文件类型

Q:之前运行 C 代码配置到 Ctrl+1 上,而 Python 运行配置到 Ctrl+3 上了,如果还有其他语言,有没有办法将多种语言的运行共用一个快捷键?

A:可以的,建立个 launch.cmd 批处理文件:

@echo off
if "%1" == "" GOTO EXIT

if "%~x1" == ".c" call "%~n1"
if "%~x1" == ".cpp" call "%~n1"
if "%~x1" == ".cc" call "%~n1"
if "%~x1" == ".cxx" call "%~n1"
if "%~x1" == ".py" python "%1"
if "%~x1" == ".pyw" pythonw "%1"
if "%~x1" == ".pl" perl "%1"
if "%~x1" == ".lua" lua "%1"

:EXIT

它会判断扩展名并使用适当的方式来运行,修改下之前配置的 Execute Program 这个工具:

Menu text: Execute Program
Command: C:\Windows\system32\cmd.exe
Argument: /C D:\Path\to\launch.cmd "$(FileName)"
Initial directory: $(FileDir)

就行了,使用 launch.cmd 去执行你的当前文件,然后 Ctrl+1 会自动判断类型并执行。

技巧2:不想把 MinGW 的 bin 目录设置到全局怎么办?

Q:假设有三四套 MinGW,不想将他们设置到全局 PATH 想避免冲突该怎么办呢?
A:写一个 mingw32.cmd 的批处理包裹以下即可:

@echo off
setlocal enabledelayedexpansion
set "PATH=d:\path\to\mingw\bin;%PATH%"
IF NOT "%1" == "" call %*
endlocal

注意将 d:\path\to\mingw\bin 改成具体的值,这个批处理会在局部设置 PATH 然后调用玩工具后又恢复,然后使用时用:

mingw32 gcc --version

就相当于设置好 PATH 再调用:

gcc --version

不会污染到外面的环境。

还可以更进一步,假设你在 D:\ 下面有 MinGW32 和 MinGW64 两套工具链,你可以只写一个 mingw.cmd 放到各自目录下:

@echo off
setlocal enabledelayedexpansion
set "PATH=%~dp0bin;%PATH%"
IF NOT "%1" == "" call %*
endlocal

两个目录里面各自放一个上面的 mingw.cmd 文件,然后用各自目录下的 mingw.cmd 启动工具,就能设置对应 mingw.cmd 所在目录的 bin 子目录进入 PATH,不用每个 mingw 环境写一遍了。

然后新建个 MinGW 的工具:

具体配置:

Menu text: MinGW32
Command: D:\Path\to\mingw32\mingw.cmd
Argument: gcc "$(FileName)" -o "$(FileNameNoExt).exe"
Initial directory: $(FileDir)

记得把 Action 设置成 Capture output 然后点击 Apply 按钮即可,此时按 Ctrl+4 激活这个 MinGW32 环境,又不需要外层去改 PATH 环境变量。

技巧三:函数列表

Q:如何在文档中间进行快速跳转呢?
A:如果要不停的切换文件编辑,那左侧就开着文件树,而如果长时间编辑一个文件的话,左侧可以切换到 Function (函数列表)功能:

点击左侧的函数可以进行快速跳转,上方输入框可以进行模糊匹配。

技巧4:CMake 文件类型

有同学反馈从 editplus 官网下载了 cmake 语法文件,设置成新的文件类别后不识别,会被识别成 .txt 的普通文本文件。

这里有两个关键点,第一是新建了 CMake 类型后,不要填写 File extensions,而是点右边的 “Advanced” 按钮:

然后填写 “An additional file name” 为 “CMakeLists.txt” 文件名,然后 OK;第二步是回到文件类型设置窗口,将新建的 CMake 文件类型用 “Up” 按钮,往前移动,移动到 Text 类型前,这样在检测 Text 类型之前,”CMakeLists.txt” 就能被提前检测出来,不会再被 Text 类型识别成普通文本文件。

好了,到这里你已经是一个 EditPlus 的 Pro 用户了。

Loading

The post EditPlus 的配置方法 appeared first on Skywind Inside.

CD4:Windows XP 开发宝典

作者 skywind3000
2024年9月2日 23:05

今天互联网上的内容,由于各种原因,正在以越来越快的速度消失,而习惯什么都从网上找的新一代网民们,却并没有备份和记录的习惯及意识。不远的将来,会有一天,当你特别想找某个工具却搜尽互联网你都找不到时,才会发现对珍贵资源做好收藏的必要性。

Windows XP 依然是一个完美的怀旧平台,它可以向后兼容到 Windows 95 的程序,是一个运行经典软件,玩经典游戏的完美方案。

图形界大佬 John Carmack 在推特上呼吁大家,现在应该有意识的保存你的开发环境,这样多年以后你想重新构建你的软件时才不会慌脚乱手,因为通常每过几年你常常会发现,自己之前的老代码已经没有合适的环境编译了:

本光盘包含了构建 Windows XP 程序所需要的必要工具,包括编译器,文本编辑器,集成开发环境和各种工具,他们全都能运行于 XP 下,并且能构建兼容 Windows XP 的项目。

制作原则:精选工具,断网可用,末日恢复,自包含无依赖,开发工具博物馆,帮你完全在 Windows XP 下工作,让你拥有 XP 下的沉浸式开发体验,容量却不超过一张 CD。

版权声明:本光碟采用 winworldpc.com 类似的版权声明,尽量收录开源或者不再销售的软件产品,目的是保护这些快绝版的资源。

光盘目录如下:

具体内容和下载地址见下文说明。

(点击 more 继续)

编译器(Compiler)

编译器的 Compiler 目录内包含以下几个工具:

目录内包含三套 MinGW 环境,分别是 mingw-2007,msys32-xp 和 w64devkit-xp三者都大有来头,详细介绍可以看同名的 txt 文件。

首先是 mingw-2007 这个压缩包,这是 MAME 模拟器开发网站 mamedev.org 在 2008 年发布的自用工具链,包含 gcc 4.2.1+3.4.5 和 nasm / gdb 等工具,同时自带 SDL1, lua, libcairo, freetype, libjpeg, libpng, zlib, ogg 等常用库,虽然只有 24 MB,但编译 2010 年以前的老项目,老游戏非常好用。

编译单个源文件的话,直接调用里面 bin 目录的 gcc 可执行编译就行了,多文件项目的话可以用里面的 mingw32-make ,是 GNU Make 的 MinGW 版本,也可用光盘里的 cmake 生成 “MinGW Makefiles”,然后用 mingw32-make 构建。

接下来是本光盘的重头戏:msys32-xp,如果你想要一套 XP 末期的比较完善的开发工具,那非它莫属了,展开后有一套完整的 msys32 环境和一套 MinGW32 环境,内含两套 gcc 5.3.0(支持 C++ 14),一套在 /usr/bin 下面用于编译 msys32 的工具(不能脱离 msys32 环境运行);另一套位于 /mingw32/bin 目录,用于编译正儿八经的 Windows 应用。

对比上面的 mingw-2007,这个 msys32-xp 不但包含更新的 MinGW32 环境,同时还有 msys32 环境,使得它可以用于构建基于 autotools / configure 的项目,如 SDL1,ffmpeg 等,传统大型项目不少都是需要 autotools 构建,那么光 MinGW 环境是搞不定的,还需要 msys 环境。它之所以包体达到 255MB,除了 msys外还包含大量基础库:

分 类 包名称
图形库 Allegro,SDL1,SDL2
网络库 libevent,libuv,libcurl,libssh2
图象库 libgif,libpng,libjpeg,png16,turbojpeg,webp
音视频 FLAC,mpg123,mp3lame,opus,speex,ogg,libmad
压缩解压 liblzma,zlib,libbz2,lzo2
数学计算 gmp,fftw3
加解密 libgnutls,openssl
文字处理 libcharset,iconv,libregex
CLI pdcurses,ncurses,libmenu,libpanel
GUI wxWidgets , wxPython
单元测试 gtest,cmocka,cppunit
其他库 python,ffi,jsoncpp

新的编译器搭配的平台 SDK 经常无法编译老项目,比如用最新 msys32+mingw32 以及配套的平台 SDK 编译 cmake-3.15 时,发现 execvp 函数的第二个参数指针类型已经不兼容了,平台 SDK 尚且如此,第三方库的接口前后兼容更是堪忧,因此 msys32-xp 里各个第三方库,基本都是选择了当年版本,避免新版本接口改变,无法直接编译老代码的尴尬。

由此可见,本光盘在制作和选择开发工具上,是十分严肃认真的,不是随便扔一个光秃秃的编译器给你,更不是给你一堆最新的库,根本无法编译老项目。

这个 msys32-xp 开发环境可以直接编译一些经典游戏,比如Yamagi 的 Quick II for Windows XP 等,所有依赖都包含在内了,并且版本契合。同时还配套一些实用工具,如:Python,tmux,lua,git,ssh,lftp,curl/wget,vim,nano,cmake,gdb 等,基本上是一个自成一体的小型独立完整开的发环境了,就算不依赖本光盘其它工具,它也能自我闭环。

第三套 MinGW 环境是 w64devkit,他是今天还在维护着的最新项目,主页在这里。该项目刻意保持对 XP 的兼容性(虽然名字里有个 64 却兼容 32 位 XP),内部包括 gcc 14.2 + gdb 和一套基于 busybox 的 shell 环境,你能用它在 XP 下面写 C++ 23 的代码,也能用 mingw32-make / cmake 来搭配构建项目,但无法编译基于 autotools 的东西。

其实 GCC 版本较新的 w64devkit 也可以同 msys 搭配编译 autotools,做法就是在 msys32-xp 解压后,进去建立个 mingw64 目录,然后把 w64devkit 压缩包解压进去,再到 bin 目录里把 busybox 导出的可执行全删除了(就是 sed/bash 那些 4KB 的),然后用 shell_mingw64.bat 进入环境,虽然叫 64,编出来还是 32 的。

也能在 msys 环境里用 mount 命令把他们临时 mount 到 /mingw32 或者 /mingw64,或者写 /etc/fstab 固化下来,而不用改动 msys32-xp 内原有目录。

最后是 Visual C++ 9.0 的命令行编译工具,对应 Visual Studio 2008,可以用来编译一些经典 VC 项目,我记得 Python 官网推荐的,他们用这个构建 Python2.7 的,使用的话在命令行运行 vcvarsall.bat 初始化环境,然后就可以使用 cl.exe / link.exe 之类的工具了,也可以用 cmake 来生成 “NMake Makefiles”,然后调用 nmake 构建项目。

本光盘主题是 XP 开发工具,那编译器自然是最重要的东西,但搭建一套真能用的 XP 开发环境,光有编译器也不够,还得有其它工具搭配使用。

文本编辑器(Editor)

编辑器目录内包含多款能运行于 XP 的经典编辑器,比如用来写程序编辑网页的 UltraEdit32:

体积小,功能强,速度快的编辑器(1.9MB),可以用来开发写代码,可以配置编译器,捕获编译错误,可以比较文件,可以做二进制编辑器,当年很流行,关于如何配置编译器,如何一键运行程序,在 UltraEdit32.txt有详细说明,一看便知。

在 XP 时代同 UltraEdit32 齐名的编辑器还有两个:EditPlus 和 NotePad++,其中 EditPlus 同样是一款小巧轻便的编辑器:

它能多 TAB 打开文档,提供文件树,能够配置外部工具,集成编译器,并且捕获编译错误,还能显示文件大纲和函数列表,打开远程文件;光盘内收录了最后一个可运行于 XP 的 EditPlus 4.30,关键它启动速度快,打开文件快,而包大小才 1.9MB。

三剑客最后就是 NotePad++ 了,版本选的 7.9.2,最后一个支持 XP 的版本(2021):

NotePad++ 是一款支持插件的编辑器,作为一张负责任的光碟,保证断网可用,末日恢复,自然不会光收录一个编辑器本体,几个核心插件自然也做成压缩包了:

把他们解压到 NotePad++ 的 plugins 目录就能使用,简单介绍下:

  • Explorer:左边显示文件树面板,收藏面板。
  • FileSwitcher:使用 Ctrl+Shift+o 快速切换文件。
  • NppExec:执行外部命令,常用于配置编译器到 Notepad++,并在下方显示编译错误。
  • NppAutoIndent:更智能的缩进。
  • NppEditorConfig:加载 EditorConfig 配置。
  • LuaScript:使用 Lua 增强 Notepad++ 功能。

这几个插件能让 NotePad++ 的易用性增加不少,属于必备插件。比较幸运的是除了 LuaScript 外,其它的最新版都能在 XP 下直接使用,所以本雷锋选择了最后一个能运行于 XP 的 LuaScript 版本,同时附带一个 launch.cmd 可以用于 NotePad++ 里 Ctrl+F5 运行各种各样不同的程序,用法见 “使用说明.txt” 。

这三款编辑器基本是 XP 时代最出名的文本编辑器了,选择一款你喜欢的,搭配前面的编译器,基本可以开始干活了。

喜欢 vim 的人,会发现目录里有 vim-9.0.0494,这是 2022 年底的版本,能在 XP 上用到 Vim 9.0 是一件很难得的事情;安装包会同时安装桌面版的 GVim.exe 和命令行版vim.exe,满足不同用途:

光有 vim 没配置也不好用,因此同目录中包含了一份我自己的离线配置和一些辅助工具,包括把用 vim 的新 tab 打开文件添加到右键菜单,交换 ctrl/caplock 的注册表文件等。

想要完全在 XP 下沉浸式工作,那么除了编写代码外,经常做的事情是什么呢?对,还需要大量阅读代码,那 XP 下读代码最好用的工具就非 Source Insight 莫属了:

这不是那种 2009 年的古董 Source Insight,而是 2019 年的版本,是 XP 在 2014 年生命终结五年后还在继续更新的版本,当然,也是最后一个能用于 XP 的 Source Insight 了。

有时在命令行下面工作,偶尔改两行配置又懒得打开 GUI 编辑器,这时命令行下的编辑器就有用武之地了,除了之前 vim 9 的命令行版本外,还收录了 nano-7.2 和只有 62KB 的 zvi (最小 vi 克隆),二者既可以运行于控制台,还能运行于 ssh/telnet 连接的远程终端,可以保存到你的 U 盘里面,极端情况下江湖救急。

编辑器目录内还有其它几款有意思的文本编辑器,欢迎自行探索。

集成开发环境(IDE)

首先是经典工具 Visual Studio 6.0 ,允许你编译一些更为古老的项目,XP 的兼容可以向后追溯到 Windows 95,基本上非常古老的程序都能在 XP 上运行,这也让 XP 成为了一个完美的怀旧平台,那么如果你想重新构建这些古老的项目,那 VC6 基本是绕不开的:

这个虽然是一个 VC6 的精简版,但核心工具一件不少(命令行构建工具,GUI 工具,ERRLOOK,DEPENDS 等),核心组件也样样齐全(MFC/CRT 包含代码),并且包含 SP6 补丁,为了让大家能够高效的在 VC6 环境下工作,这个版本还附带了几款广受好评的插件,比如 VAX 和 WndTab,都是当年的版本,现在想找恐怕都很难找到。

安装的话很简单,参考压缩包里的 “使用说明.txt”,比如以 VAX 为例,去到 plugins 目录内的 VAX 目录,运行 Reg.bat 进行注册,重启 MSDEV.EXE 就行了:

红番茄(VAX)安装完毕,进去后工具栏多了一行红番茄的专属按钮:

可以支持文件快速切换(Alt+Shift+o),符号查找,补全增强,代码片段等现代 IDE 功能,让 VC6变得更顺手;不想用的话随时运行 UnReg.bat 注销。

其它 VC6 相关工具和插件可以自行探索压缩包,不要忘记阅读里面的 “使用说明.txt”。

觉得 Visual Studio 6.0 太老了没关系,光盘 IDE 目录内另一个软件,就是 Orwell 版本的 Dev-C++ 5.11,这是最后一个正统的 Dev-C++ 了,后面就由 Embarcadero 公司(目前 Delphi / Rad Studio 那家母公司)接手了:

默认配套的工具链是 TDM-GCC 4.9,支持 C++ 11,同时允许你添加其它工具链,比如你可以添加之前 Compiler 目录内的其它 MinGW 工具来编译,除了不能像 VC6 一样写 MFC 画对话框,其它 Win32 / Console 程序都支持。

而如果有一些界面小工具要开发又不想写 Win32/MFC 怎么办呢?试试 SharpDevelop:

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

而 SharpDevelop 4.4 是最后一个支持 XP 的版本,XP 原版支持 .NET Framework 3.5,而 XP SP3 支持 .NET Framework 4.0,在 SharpDevelop 里创建新项目时可以选择:

默认直接上 4.0 即可;目前本光盘提供了几套开发 GUI 界面程序的方式:

  • Win 32 裸写:所有工具都支持;
  • MFC:用 Visual Studio 6.0 才行;
  • wxWidgets:用 msys32-xp 里的 MinGW 环境。
  • wxPython:用 msys32-xp 里 MinGW 里的 python 开发。
  • WinForm:用 SharpDevelop + C# 开发。

应该满足大部分 GUI 开发需求了,目前 IDE 目录内三套软件,每套软件本雷锋都附了文本文件,包含用法说明:

都是经典版本,兼具收藏和使用价值。

工具目录(Tools)

光盘的 Tools 目录内容如下:

光盘内有很多压缩包,为了保证断网可用,自然包含了两个解压软件:7z2408(这是 24 年8月份的最新版本),和 WinRAR 6.0(最后可用于 XP 的版本),即便你在一台全新的 XP 机器上也可以无依赖的解压光盘内其它内容。

系统工具有 DTLite4356,即 XP 下面经典的虚拟光驱软件 DAEMON Tools 的 Lite 版本,这样本光盘如果以 iso 形式传递也能在 XP 下直接打开;还有一个是 MenuMgr1.2.exe,可以用来清理鼠标右键菜单的;还有一个工具是 ProcExp,没错就是那个好用的任务管理器,最新版居然支持还 XP,那直接收录。

开发方面的工具自然包括 cmake-3.13.5(3.14 就放弃 XP 了),解压后把 bin 目录添加到PATH 环境变量就能使用;先前的 msys32-xp 的 /mingw32/bin 下的 cmake 版本只有 3.2,想搭配使用这个的话把 cmake 的 bin 添加到 PATH 最前面,先于 /mingw32/bin 就行;最后还有个构建工具 emake,采用类似 IDE 的定义式构建方式,非命令式,中小项目非常方便,欢迎尝试。

我写程序喜欢 log 调试,但是 Windows 下缺乏一个好用的日志实时查看工具,于是我收录了三个比较小巧的工具:TailW32,baretail 和 mtail,从简单到复杂,第一个重在体积超小,左边有文件列表方便切换;第二个特点是多 tab 日志同时打开监控,关键字上色;第三个是功能最强的,还有报警功能。

其它的 DEPENDS.EXE 用于查看可执行和动态库的依赖关系,ERRLOOK.EXE 用于查询 Windows 错误码的文字含义,都是 Windows 下开发经常使用的工具,这两个虽然都来自 VC6 的安装盘,但比最新版好用很多,最新版 DEPENDS.EXE 碰到复杂的依赖关系或者循环依赖关系会计算半天,有时还会界面卡死不动,远不如光盘里的版本好用。

最后是我个人多年的珍藏,自己不断整理维护的工具包,也是工具目录压箱底的东西:

这是我个人多年整理的 gnu-tools-xp.rar 工具包,别看它只有 4.5MB,内容却毫不含糊:

相信很多人都用过 busybox,这是 Win32 版本的,内部包含 172 个 unix 工具:

用法就是:“busybox <工具名> [参数]” ,比如要调用里面的 ls 工具查看目录:

busybox ls -la

它就能像 linux 下的 “ls -la” 命令一样,按 ls 的格式列出当前目录下面的文件,这个版本的 busybox 基本包含了所有 GNU 必备工具了,其中一些较为复杂的有:

  • ash:busybox 版本的一款 shell,基于 ash。
  • awk:用于模式匹配和文本处理的一门脚本语言。
  • sed:流式文本编辑器,多用于自动批处理修改文本。
  • vi:很强大的文本模式可视化编辑器,能运行于远程终端,这里有一份教程

基本上一个 busybox 就能搞定所有在 Windows下运行 GNU Tools的需求,更多使用帮助可以访问它的项目主页:https://frippery.org/busybox/

这个 gnu-tools.rar 压缩包里有不少独家的东西,比如 cscope.exe,搭配 vim 可以把 vim 变成一个 C 语言的 source insight,它可以分析项目代码,建立数据库,然后帮你查找符号的定义和引用,网上下得到的是 15.8a 的 Windows 可执行,但作者后来发布过一个 15.8b 的修正版,改了不少 bug,不过不能运行于 Windows,本雷锋花了两天移植,这是全网最新的Windows 版本。

另一个代码分析工具 ctags.exe 也是众多编辑器里必备的工具,用于 Vim 里显示当前文档的函数列表,用于定义查找和跳转,支持 65 种编程语言,用的最新的 universal-ctags,官网的可执行早已不能运行于 XP,这是我幸苦找到的一个版本。

接下来是 edlin,这是 DOS 时代官配的行文本编辑器,原版本已经无法运行于后来的 Windows 系统了,这个是后来由 Gregory Pietsch 移植的 Win32 版,同样支持 XP;当然,虽然没啥实用价值,但有收藏和怀旧的价值。

接下来是著名的终端文本编辑器 nano:

这个编辑器历史悠久,先祖可以追溯到 unix 下的 pico 编辑器,遗憾的是早在 N 年前的 nano-4.9 时就放弃了对 Windows XP 的支持,本着不抛弃不放弃的原则,本雷锋 backport 了 2024 年夏季的最新版 nano-7.2,它不但能运行于 Win XP -> Win 11 的所有命令行,还能运行于远程终端,当你用 ssh/telnet 连接一台 Windows Server 或者 Server Container 时,可以在远程终端里运行它。

另一个 zvi 编辑器同 nano 类似,一样运行于 Win XP -> Win 11 所有系统的命令行,同样支持远程终端和无桌面的 Server Container,这是 busybox 里面的 vi 编辑器,被我扣出来作为独立可执行了,满足 vim 用户的使用习惯,大小只有 62KB,可谓最小 vi 了:

欢迎关注项目主页:https://github.com/skywind3000/zvi

放两张截图,运行于 Windows XP 下:

使用 PuTTY 连接 Windows Server 运行 zvi 于远程终端中:

可以把这个 zvi(62KB)和前面的 nano(87KB)放到你的 u 盘里,关键时候进行救急用。

版本控制工具(VCS)

既然要搭建正儿八经的开发环境,版本管理工具自然不能少:

有独立的 Git 工具,也有 TortoiseGit,我听说不少游戏公司现在还在用 SVN,所以放了个 TortoiseSVN(包含命令行 svn),都是最后能够支持 Windows XP 的版本。

脚本语言(Script)

光盘的 Script 目录中包含了 Python 和 Lua 的新老版本:

XP 下面任然有不少程序是用脚本语言开发的,有些还会用到老版本的解释器,比如有很多 Python2 的老代码,因此这里同时包含了 Python-2.7 和 Python-3.4.4(最后一个兼容 XP 的 Python),兼顾老代码与新代码。

同样 Lua 的选择上也在包含了传统经典版本 5.1 的同时收录了最新的 5.4.7,都包含了二进制可执行和源代码。

网络工具(Network)

当你需要进行远程开发,登陆远程服务器时,少不了终端软件:

这里有 2024 年最新的 putty-0.81(感谢 putty,多年一直保持 XP 的兼容性,让我们现在都能在 XP 下用到最新版),还有很多人喜爱的 MobaXterm 绿色版。

附带了个迅雷精简版,我珍藏的一个版本,体积小界面清爽:

另外还有其它几个经典小工具,欢迎自行探索。

媒体相关(Media)

光盘的 Media 目录下包含如下软件:

本光碟制作原则就是考虑完全在 XP 下工作的情况,那么开发除了写代码,当然需要读文档,因此收录了 FoxitReader 早年的一个绿色单文件版本(非自解压版):

不是那种自解压程序,每次运行需要生成一堆临时文件,就是纯的绿色单文件,仅 5MB。

另外还有 ACDSEE 最经典的一个版本 2.44,任然是绿色单文件,方便查看老游戏的资源文件。为了提升工作时的专注力,有时候写着代码看着文档想听点音乐怎么办呢?

请出我们的老朋友:Winamp,版本选择的是 5.6 Lite 版本,XP 下面听音乐的最佳搭档;听说他们最佳还出了一个最后的版本 5.80-build-3660,我试了一下,没法再 XP 下安装了,因此这个 5.6 Lite 应该是 XP 下最后的 Winamp 了。

最后还有个 FlashPlayer 14.0,可以播放大部分怀旧 Flash 动画,可以玩大部分 Flash 游戏(Flash 游戏主要是集中在 9-14 版本号),兼容性最好。

到此为止《XP 开发工具》这张 CD 的内容基本制作完成了,最后发布前我突然在想,前面的内容都太严肃了,是不是放几个轻松的小游戏调剂下?免得程序员等待编译时无聊,于是增加了一个 “赠品” 目录:

都是一些经典的小游戏,直接运行无需安装,其中有汪海涛的《潜艇大战》,是个 MFC 程序,玩家控制军舰左右移动,用深水炸弹炸潜艇:

别看这个 “潜艇大战.EXE” 文件只有 404KB,当年学生时候我却玩的不亦乐乎,几次重装电脑都舍不得删除,一直保留到现在,今天玩起来依然很有意思,十分解压。

另外几个都是精挑细选的经典 Flash 游戏,可以用 Media 目录里的 FlashPlayer 14 来播放,最好拷贝到 C:\Program Files 然后设置下 .swf 文件默认由它来打开,然后就能双击运行:

觉得潜艇游戏节奏快可以试试这款比较休闲的麻将,制作很精良,全程语音;有了这几个小游戏在这里,等编译时再也不会无聊了,即便没网络,也可以放松一下,换换脑子。

网络资源

本光盘最开始我准备了两张光盘的内容,然后精挑细选,反复比较,留下了最精华的一张光盘内容,这么多东西,总体 ISO 大小只有 649MB,小于一张 CDR 的容量。

选择制作成 650MB 以内的 ISO,一方面是便于收藏和分享,另一方面是强迫自己有所取舍,留下真正精品的东西;虽然,还是有一些我觉得很有意思的东西没机会收录,所以我在 github 上新建了一个叫做 abandonware 的网站,把他们做成额外 DLC 陆续放上去。

本光盘的 DLC 专用网址:

最后是一些 Windows 开发用得上的资源:

下载地址

光盘下载:

  • 谷歌:WindowsXP-Devkit
  • 度盘:地址 s/1fWWskQYbbk0Eemp3eSFU2Q,口令 z8gd
  • 115:地址 s/swh3bpl3ze9,口令 8866
  • MD5SUM: fcdd640440bfbf0304338a8c70d7bea4

避免机器人扫描,读盘地址前请自己拼接 https://pan.baidu.com/ ,115 自行添加 https://115.com/ 即可。

《绝版软件保护工程》永久地址:

更多信息,类似光盘请访问上述官网。

往期回顾:

Loading

The post CD4:Windows XP 开发宝典 appeared first on Skywind Inside.

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

作者 skywind3000
2024年8月29日 01:05

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

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

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

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

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

(点击 more 展开阅读)

第一:定义式构建工具

简单点例子:main.mak 就三行:

flag: -Wall, -O3
mode: exe
src: foo.c, bar.c, main.c

第一行设定编译参数,第二行指明目标格式,第三行设定源代码,这也是大部分时候写点小玩具,小测试的样子,然后:

emake main.mak

就能生成 main.exe 了,定义式的意思就是不像 cmake 一样每次要在 CMakeLists.txt 里写个小程序,而是跟 IDE 一样定义好源文件,设定好 release/debug 的编译参数,然后 emake 帮你做好默认工具链初始化,依赖分析,多核编译等等一堆琐事。

而且还没有 cmake 的 rm -rf build && mkdir build && cmake -B build -G "MinGW Makefiles" . 初始化环节,这个初始化每次都很蛋疼,极大的阻碍了我创建新项目的热情。更不用在 build 目录生成一大堆 shit,写好 emake 工程文件就能直接编译出可执行了,没有任何第二层构建工具的依赖。

也不会每个项目像 cmake 一样单独占用一个目录,我一堆小项目全部放一个目录里都没事情。到这里你可能会说,好像也没比 xmake 简单多少啊?别急我们还能继续简化。

第二:零工程文件

如果你连工程文件都懒得写,没关系,emake 支持以 docstring 的形式将工程配置写到代码中:

#include <stdio.h>

//! flag: -Wall, -O3
//! src: foo.c, bar.c
int main() {
    ...
}

这些以 //! 开头的注释可以用来描述项目配置,然后:

emake main.c

就能生成 main.exe 了,连工程文件都不需要,你把上述命令配置到 vscode / vim 里,按 F9 就能编译一个简单的项目,连 “mode: exe” 都不需要写,默认值即可,需要第三方库了就在加一行:

//! link: zlib, m

就能链接对应的 .a 静态库了。

复杂的项目都是由一个个小的想法组成的,在写复杂项目之前,往往有大量的小的想法需要验证,小的模块需要开发,对于 “编码-构建-测试” 这个内循环,属于核心工作流,任何一个环节改进一点,都能带来极大的效率提升,这种 docstring 内嵌的方式,可以极大的简化你验证想法的成本。

还能降低你创建新项目的心理门槛,想建就建,你会发现你更容易开始一个新模块的开发了,等到模块变得足够复杂了,再将这些 docstring 独立出来,放到一个统一的工程文件里。

看到这里也许你会觉得是简化了一些,但 xmake 之类的对简单项目好像也不复杂啊?

别急,我们把需求稍微增加点,支持跨平台编译,支持区别 debug/release 区别就明显了。

第三:条件编译

有个模块需要在 Windows / Linux 下运行,需要区别 debug/release 该怎么写?简单:

src: src/main.c
win32/flag@release: -O3, -Wall
win32/flag@debug: -Og, -g
linux/define: TEST1, TEST2

分别在 Windows 平台的 debug/release 两种配置中定义了不同的 flag ,并在 linux 下面定义了两个宏(不区分 debug/release),编译时:

emake --profile=release main.mak    # 按照 release 模式编译
emake --profile=debug main.mak      # 按照 debug 模式编译

只有四行配置,搞定跨平台多模式构建。

横向对比下 xmake:

target("main")
    set_kind("binary")
    add_files("src/main.c")
    if is_plat("macosx", "linux") then
        add_defines("TEST1", "TEST2")
    end
    if is_plat("windows") then
        if is_mode("release") then
           add_cxflags("-O3")
        else
           add_cxflags("-Og")
        end
    end

号称简单的 xmake 立马不那么简单了,同样 cmake 也没好到哪里去:

project(main)
add_executable(main 
    src/main.c
)
if (WIN32)
    set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Og")
    set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O3")
endif()
if (LINUX)
    target_compile_definitions(main PRIVATE TEST1 TEST2)
endif()

对比下前面 emake 的 4 行工程文件,哪种更简单?你更想写哪种?需求稍微完善点,那些号称简单的构建工具们,一个个都原形毕露了。

第四:第三方包引入

这个更简单:

package: python3, sqlite3, curl

就行了,emake 会使用 pkg-config 将这几个包的 .pc 文件信息提取出来并分析他们的依赖图(比如会进一步补充 lzma, openssl 之类的 python3 依赖),然后添加到 CFLAGSLDFLAGS 上去。

如果你的 /usr/lib/pkgconfig 下面没有对应 .pc ,比如你的第三方库随意乱放,没 install 时,当然也可以手工指定 include 目录,和静态库链接选项,但库多了会略显繁琐。

第五:多套工具链切换

默认工具链配置位于 ~/.config/emake.ini ,默认不提供的话 emake 能自动搜索 $PATH 里的工具,其它工具链位于 ~/.config/emake/{name}.ini 使用时用 --cfg={name} 指明:

emake --cfg=mingw32-gcc12 <arguments>

就会读取 ~/.config/emake/mingw32-gcc12.ini 的工具链配置进行初始化了,一般程序员机器上都会不止一套工具链,比开发 Windows 下 32/64 位的程序,或者交叉编译 android 平台等。

配置一个新的工具链很简单,只需要在 ini 文件里写入:

[default]

# 工具链的 bin 目录,用于查找 gcc / clang 等工具
home=d:/msys32/mingw32/bin

# 当你有多套工具链时,不可能都加入 $PATH,这个配置可以让 emake 在
# 构建时临时追加到 $PATH 前面,不污染外层父进程的环境变量
path=d:/msys32/mingw32/bin,d:/msys32/usr/bin

# 通用配置,免得每个工程文件写一遍
flag=-Wall
link=stdc++, winmm, wsock32, user32, ws2_32
cflag=-std=c11
cxxflag=-std=c++17

# 针对 debug/release/static 三种 profile 的设置,使用
# emake --profile=<name> xxx 在构建时指明使用啥 profile
define@debug=_DEBUG=1
define@release=_RELEASE=1
define@static=_STATIC=1, _RELEASE=1

flag@debug=-Og, -g, -fno-omit-frame-pointer
flag@release=-O3
flag@static=-O3, -static

# 多核编译
cpu=4

# 目标平台名称,不提供得话默认用 python 的 sys.platform 字符串代替
target=win32

# 条件编译时候的条件变量,在工程文件里可以用 win32/flag: xxx 来使用
name=win32,nt,have_openssl

其实不用写那么多,平时只要 homepath 两项定义好就行,后面的都是一些公共配置的演示,可以进一步帮助少写点工程文件。

写好多个 ini 文件放到 ~/.config/emake 目录下面的话,就能随时切换多套工具链:

emake --cfg=mingw32-gcc12  project.mak     # 32 位 Windows 程序,GCC-12
emake --cfg=mingw64-gcc12  project.mak     # 64 位 Windows 程序,GCC-12
emake --cfg=mingw32-gcc53  project.mak     # 用 gcc-5.3 工具链构建 Windows XP 兼容程序。
emake --cfg=clang32  project.mak           # 使用 clang 构建
emake --cfg=android-16-arm  project.mak    # 交叉编译 Android 16 的 arm 程序
emake --cfg=android-16-x86  project.mak    # 交叉编译 Android 16 的 x86 程序

我机器上有一堆工具链,随时随地,想切就切。

这个 emake 我从 2009 年不断迭代使用至今,用它构建过最复杂的项目是 webrtc,是的,我没用 google 那套,并且跨平台支持 android/windows/iOS。

虽然我对外的项目都会写一个 CMakeLists.txt,但同一个项目内也会包含一个 emake 工程文件我自己开发时使用,新项目一开始就用,直到两个月后足够复杂了,项目快完成了,我再写一个 cmake 的工程文件准备发布。

平时我自己验证一些想法,写一些小模块,做些中小项目也都是用它,配置在 vim/vscode 里,一键编译,很趁手,也没一直开发,陆陆续续更新了很多年,感觉可以拿出来见见人了。

项目地址:

限于篇幅,还有很多功能没法说完,比如:

  • 如何同 vcpkg 联动,导入 vcpkg 包?
  • 如何导出 compile_commands.json ,供其它工具使用?
  • 如何使用 clang 等非 gcc 工具?
  • 如何构建动态库和静态库,如何给动态库导出 MSVC 能用的 .lib 文件?
  • 如何位单个源文件指定不同的编译参数?
  • 如何添加汇编语言源文件?
  • 如何在 Windows 项目中加入 .RC 资源文件?
  • 之类的,欢迎参考项目文档。

最后说一句:我觉得 emake 的最大价值在于它放弃了追求全宇宙,在特定的需求范围内寻求最优解,是一个很趁手的二号构建工具。

Loading

The post Emake:你见过最简单的 C/C++ 构建工具 appeared first on Skywind Inside.

❌
❌