Skip to content

博客

Zig Wasm Memory Canvas

展示HTML canvas、wasm memory 和 zig 交互的一个小示例。

这个示例来自https://github.com/daneelsan/minimal-zig-wasm-canvas,而这示例本来修改来自https://wasmbyexample.dev/examples/reading-and-writing-graphics/reading-and-writing-graphics.rust.en-us.html。对于当前这个本身,它如大多数 zig 项目一样,zig 命令是你唯一的依赖项。

main.zig 定义了一个全局的 8X8 像素数组: checkerboard_buffer。并导出三个函数:getCheckerboardBufferPointer()getCheckerboardSize()colorCheckerboard(...), getCheckerboardBufferPointer() 返回一个指向checkerboard_buffer开始处的指针,这将在script.js 被使用。getCheckerboardSize() 返回棋盘格子数量。 colorCheckerboard(...)函数根据传入的颜色参数,来改变棋盘格的颜色。

main.zig 通过build.zig被编译成 wasm模块 checkerboard.wasm

index.html 定义了一个canvas,并指定样式:image-rendering: pixelated; width: 50%,这是因为main.zig是定义的一个8x8 象素的棋盘格,所以需要通过 CSS属性image-rendering 来对 canvas 画出的图像进行放大显示,以获得一个清晰的棋盘格来清楚的展示颜色的变化。

script.js 文件创建了一个名为memoryWebAssembly.Memory ,它存储的是WebAssembly实例所访问内存的原始字节码。下一步是使用WebAssembly.instantiateStreaming()方法编译和示例化获取到的wasm模块,返回的一个Promise对象,它包含一个instanace字段(WebAssembly.instanace) , 它保存有从checkerboard.wasm 导出的所有方法。

接下来是:

  • WebAssembly.Memory memory 创建一个Uint8Array数组:wasmMemoryArray

  • 获取到index.html中定义的canvas checkerboard, 并使用getCheckerboardSize()获取到的棋盘格数来指定canvas 的widthheight,接着使用canvas的context 创建一个 ImageData 对象:ImageData

  • 在循环中更新棋盘格的颜色:

    • 传入RGB颜色参数,调用colorCheckerboard(...),这会修改checkerboard_buffer中保存的值
    • checkerboard_buffer中的内容,是一个段wasmMemoryArray,可以通过getCheckerboardBufferPointer获取内存起始地址 和 8 _ 8 _ 4 字节的偏移长度来获取
    • 将获取到的内容,放入到imageData
    • imageData 写入到canvas

build.zig 指定的为一个构建目标是:wasm32-freestanding-musl

用于构建项目的最新zig版本是:

Terminal window
$ zig version
0.12.0-dev.817+54e7f58fc

构建wasm 模块,运行:

Terminal window
$ zig build
$ ls zig-out/lib/
zig-wasm-canvas.wasm

注意: 为了运行将内存导入到 WASM 二进制文件中,构建时需要指定--import-memory参数

在项目目录上,启动一个服务:

Terminal window
$ python3 -m http.server

这时在你的浏览器中浏览:localhost:8000,就可以看到棋盘格在改变颜色。

Zig in Webassembly

Zig 是一种通用编程语言和工具链,用于维护健壮、优化和可重用的软件,Zig 支持开箱即用的 Webassembly 构建,Zig 的实现是通过使用 LLVM 来提供编译目标,对Webassembly System Interface(WASI) 的支持尚处于积极开发中,可以参见官方文档

Zig 从 0.4.0 开始提供对 WebAssembly 的实现

WebAssembly 是一种运行在现代网络浏览器中的新型代码,并且提供新的性能特性和效果。它设计的目的不是为了手写代码而是为诸如 C、C++和 Rust 等低级源语言提供一个高效的编译目标。

Webassembly 1.0 已经在4个主要浏览器引擎中发布Roadmap

为了进一步推动模块化 WebAssembly 生态系统,Mozilla、Fastly、英特尔和红帽公司携手成立了ByteCode Alliance(字节码联盟)。ByteCode Alliance 是一个非营利组织,致力于在 WebAssembly 和 WebAssembly System Interface(WASI) 等标准的基础上创建安全的新软件基础。

作为 W3C WebAssembly Community Group 中的一项开放标准,WebAssembly 是为下列目标而生的:

  • 快速、高效、可移植——通过利用常见的硬件能力,WebAssembly 代码在不同平台上能够以接近本地速度运行。
  • 可读、可调试——WebAssembly 是一门低阶语言,但是它有确实有一种人类可读的文本格式(其标准即将得到最终版本),这允许通过手工来写代码,看代码以及调试代码。
  • 保持安全——WebAssembly 被限制运行在一个安全的沙箱执行环境中。像其他网络代码一样,它遵循浏览器的同源策略和授权策略。
  • 不破坏网络——WebAssembly 的设计原则是与其他网络技术和谐共处并保持向后兼容。

更多参见: MDN Webassembly概念

1 life.zig 导出了三个函数

// Advance the world by strong mutation
export fn advance() u32 {
....
WORLD.cells = cell_buf;
return num_changed;
}
export fn set_cell(index: u32) void {
if (index > NUM_CELLS) return;
WORLD.cells[index] = .Alive;
}
export fn get_char(index: u32) u32 {
if (index > NUM_CELLS) return '◻';
return switch (WORLD.cells[index]) {
.Dead => {
return '◻';
},
.Alive => {
return '◼';
},
};
}

2 在 life.js 中会调用这些 WASM 导出的方法

// Load WebAssembly module `life.wasm`
// https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/instantiateStreaming
Game = await WebAssembly.instantiateStreaming(
fetch("life.wasm"), importObject
)
set_cell = Game.instance.exports.set_cell;
advance = Game.instance.exports.advance;
get_char = Game.instance.exports.get_char;
wasm_loaed = true
...

3 在 index.html 加载life.js

<html lang="en">
<head>
<title>Life Demo</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<input type="button" id="button" label="button" value="BUTTON" />
<pre id="life_pre"></pre>
</body>
<script src="life.js"></script>
</html>

以上这些就是使用Zig 创建 WebAssemble 程序的代码。(非常感谢sleibrock/zigtoys)

4 构建 life.wasm

Terminal window
$ zig build-lib life.zig -target wasm32-freestanding -dynamic -rdynamic -O ReleaseSmall

更多构建说明参见 Ziglang.org

5 运行

启动一个本地web server:

Terminal window
$ python3 -m http.server

在浏览器中,浏览:http://localhost:8000 demo

  1. Webassembly
  2. Webassembly developer guide
  3. MDN Webassembly
  4. Zig language Reference
  5. WebAssembly with Zig, Part 1
  6. WebAssembly With Zig, Pt. II
  7. ByteCode Alliance

Znap + ohmyzsh

⚡️ Znap! Fast, easy-to-use tools for Zsh dotfiles & plugins, plus git repos

在这之前一直使用的是https://ohmyz.sh/,使用一个大仓库集中管理了 所有常用的 Zsh 配置、插件、主题,而且管理的非常出色,也的确很好用,只是在安装仓库以外的插件时,不是很友好。此时如果需要的是一个通用的插件管理框架,znap 是一个不错的选择,将ohmyz视为插件仓库,搭配起来用,有一个非常不错的体验。

Oh My Zsh is a delightful, open source, community-driven framework for managing your Zsh configuration. It comes bundled with thousands of helpful functions, helpers, plugins, themes, and a few things that make you shout…

将下面的内容添加到~/.zshrc,其中~/Repos可以修改为你想要任意的一个目录来保存插件或者 Git 仓库。

# Download Znap, if it's not there yet.
[[ -r ~/Repos/znap/znap.zsh ]] ||
git clone --depth 1 -- \
https://github.com/marlonrichert/zsh-snap.git ~/Repos/znap
source ~/Repos/znap/znap.zsh # Start Znap

此时,只需要重启 Terminal 即可完成 snap 的安装。

卸载 snap 只需要将上面的配置从~/.zshrc 中移除,并删除~/Repos/即可。

Terminal window
$ znap install sindresorhus/pure
$ znap install marlonrichert/zsh-autocomplete
$ znap install zsh-users/zsh-autosuggestions
$ znap install zsh-users/zsh-syntax-highlighting

使插件生效,需要在~/.zshrc 添加配置:

Terminal window
znap source marlonrichert/zsh-autocomplete
znap source zsh-users/zsh-autosuggestions
znap source zsh-users/zsh-syntax-highlighting

卸载一个或多个插件可以使用uninstall,如:

Terminal window
$ znap uninstall asdf-vm/asdf ohmyzsh/ohmyzsh

更新 snap、安装的plugins 只需要运行:

Terminal window
$ znap pull

znap install sindresorhus/pure 下载好主题后,在~/.zshrc 使之生效:

# `znap prompt` makes your prompt visible in just 15-40ms!
znap prompt sindresorhus/pure

也可以直接使用ohmyz中的主题,此时需要先下载ohmyz:

Terminal window
$ znap install ohmyzsh/ohmyzsh

然后在~/.zshrc配置加载必要ohmyz lib,再使用znap prompt指定主题:

znap source ohmyzsh/ohmyzsh lib/{git,theme-and-appearance}
znap prompt ohmyzsh/ohmyzsh robbyrussell
znap source ohmyzsh/ohmyzsh lib/{git,theme-and-appearance} plugins/git plugins/zoxide plugins/macos

上面等同于在ohmyz中使用插件 plugins=(git macos zoxide)

# Download Znap, if it's not there yet.
[[ -r ~/Repos/znap/znap.zsh ]] ||
git clone --depth 1 -- \
https://github.com/marlonrichert/zsh-snap.git ~/Repos/znap
source ~/Repos/znap/znap.zsh # Start Znap
# `znap prompt` makes your prompt visible in just 15-40ms!
znap prompt sindresorhus/pure
# plugins=(git macos zsh-autosuggestions zsh-syntax-highlighting zsh-autocomplete zoxide)
# `znap source` starts plugins.
znap source marlonrichert/zsh-autocomplete
znap source zsh-users/zsh-autosuggestions
znap source zsh-users/zsh-syntax-highlighting
# `znap prompt` also supports Oh-My-Zsh themes. Just make sure you load the
# required libs first:
znap source ohmyzsh/ohmyzsh lib/{git,theme-and-appearance} plugins/git plugins/zoxide plugins/macos
#znap prompt ohmyzsh/ohmyzsh robbyrussell
  1. the included .zshrc file.

Zig初体验

遇到一个小众而又趣的开发语言Zig,目前还在开发中,初步实现自举,目前没有发布1.0版本,当前发布的最新版本是0.10.1。

Zig 可以在项目直接加入 C 代码实现混编,Zig 可以作为 C/C++ 语言的编译器。

Zig 不支持宏编程,但支持comptime关键字,在编译时运行代码和惰性求值,编写通用代码或是进行元编程。

Zig 没有像 Rust 那样实现内存安全,可 Zig 也没有 Rust 那么复杂。

Zig 内置测试功能,还自带很完备的命令工具,这点是不是和 Go 很像?

Terminal window
$ zig
Usage: zig [command] [options]
Commands:
build Build project from build.zig
init-exe Initialize a `zig build` application in the cwd
init-lib Initialize a `zig build` library in the cwd
ast-check Look for simple compile errors in any set of files
build-exe Create executable from source or object files
build-lib Create library from source or object files
build-obj Create object from source or object files
fmt Reformat Zig source into canonical form
run Create executable and run immediately
test Create and run a test build
translate-c Convert C code to Zig code
ar Use Zig as a drop-in archiver
cc Use Zig as a drop-in C compiler
c++ Use Zig as a drop-in C++ compiler
dlltool Use Zig as a drop-in dlltool.exe
lib Use Zig as a drop-in lib.exe
ranlib Use Zig as a drop-in ranlib
objcopy Use Zig as a drop-in objcopy
env Print lib path, std path, cache directory, and version
help Print this help and exit
libc Display native libc paths file or validate one
targets List available compilation targets
version Print version number and exit
zen Print Zen of Zig and exit
General Options:
-h, --help Print command-specific usage

⚡ 一种简单的语言

专注于调试你的应用程序,而不是调试你的编程语言知识

  • 没有隐式控制流
  • 没有隐式内存分配
  • 没有预处理器,没有宏

⚡ 编译期代码执行

基于编译期代码执行和惰性求值的全新元编程方法

  • 编译期调用任意函数
  • 在没有运行时开销的情况下,将类型作为值进行操作
  • 编译期模拟目标架构

⚡ 用Zig维护代码

逐步改善你的C/C++/Zig代码库

  • 将Zig作为一个零依赖的,支持开箱即用交叉编译的C/C++编译器
  • 利用 zig build在所有平台上创建一个一致的开发环境
  • 在C/C++项目中添加一个Zig编译单元,跨语言LTO默认启用

官网文章 深入了解 从系统编程的角度对 Zig 进行深入的功能了解,有了 C++、D 和 Rust,为什么还需要 Zig? 介绍了Zig 同其他语言的区别。

ziglings 项目可以通过修补和调试一系列带有故障的小程序来学习 Zig 。

ziglearn.org 有一份 Zig 在线学习教程。

创建hello.zig 文件,内容:

const std = @import("std");
pub fn main() void {
std.debug.print("Hello, {s}!\n", .{"World"});
}

使用命令zig run hello.zig 来构建和运行:

Terminal window
$ zig run main.zig
Hello, World!

也可以通过Zig 内置命令来完成,运行:

Terminal window
mkdir hello-world
cd hello-world
zig init-exe

输出:

Terminal window
info: Created build.zig
info: Created src/main.zig
info: Next, try `zig build --help` or `zig build run`

运行 zig build run 应该会编译成可执行文件并运行,最终结果将会是:

Terminal window
$ zig build run
All your codebase are belong to us.
Run `zig build test` to run the tests.

官方支持的一个language server 是 zls ,目前主流编辑器都有Zig 的语法高亮支持、自动补全、定义跳转等。

Zig 自带构建相关命令行工具:

  • zig build 通过build.zig 构建项目(build project)
  • zig init-exe 初始化一个zig build 应用程序项目
  • zig init-lib 初始化一个 zig build 类库项目

Zig 提供四种编译模式,Debug模式是默认模式,因为它需要的编译时间最短。

运行时安全优化
DebugYesNo
ReleaseSafeNoYes, Speed
ReleaseSmallNoYes, Size
ReleaseFastNoYes, Speed

这些可以在zig runzig test 命令中,通过使用参数 -O ReleaseSafe, -O ReleaseSmall-O ReleaseFast 来启用。

zig build-exe, zig build-lib, zig-obj 分别用于构建输出 可执行文件、lib文件、obj文件,这些命令都接受源文件和参数。

几个常见参数:

  • -fsingle-threaded 断言二进制文件是单线程的, 这会导致如互斥锁之类的线程安全措施变成空操作。
  • -fstrip 从二进制文件中移除调试信息
  • --dynamic它与 zig build-lib 结合使用以输出动态/共享库。

如对上面的hello.zig 执行命令 zig build-exe ./main.zig -O ReleaseSafe -fstrip -fsingle-threaded 生成一个24k可执行的文件,而使用命令zig build-exe ./main.zig -O ReleaseSmall -fstrip -fsingle-threaded生成的可执行文件只用14k。

hello.zig
const std = @import("std");
pub fn main() void {
std.debug.print("Hello, {s}!\n", .{"World"});
}

默认情况下,Zig 会根据你的 CPU 和操作系统进行编译。这可以通过-target参数进行覆盖,可以通过下面的命令将hello编译到64位arm linux 平台:

Terminal window
zig build-exe ./main.zig -O ReleaseSmall -fstrip -fsingle-threaded -target aarch64-linux

可以对一下的CPU架构交叉编译:

  • x86_64
  • arm
  • aarch64
  • i386
  • riscv64
  • wasm32

可以对一下操作系统进行交叉编译:

  • linux
  • macos
  • windows
  • freebsd
  • netbsd
  • dragonfly
  • UEFI

-target 参数格式是: <arch><sub>-<os>-<abi>,通过运行命令zig targets 可以找到可以支持的架构、操作系统、CPU、ABI详细信息。

Terminal window
$ zig targets | jq "keys"
[
"abi",
"arch",
"cpuFeatures",
"cpus",
"glibc",
"libc",
"native",
"os"
]
$ zig targets | jq ".native"
{
"triple": "x86_64-macos.13.3.1...13.3.1-none",
"cpu": {
"arch": "x86_64",
"name": "skylake",
"features": [
"64bit",
"adx",
"aes",
"allow_light_256_bit",
"avx",
"avx2",
"bmi",
"bmi2",
"clflushopt",
"cmov",
"crc32",
"cx16",
......

如: 对于M1架构的 MacOS 可以使用aarch64-macos, Intel x86 CPU版 MacOS 使用x86_64-macos-none,x86 64 位CPU Windows 使用x86-windows-gnu,x86 架构64位CPU Linux 使用x86-linux-gnu:

Terminal window
$ cat main.zig
const std = @import("std");
pub fn main() void {
std.debug.print("Hello, {s}!\n", .{"World"});
}
$ zig build-exe ./main.zig -O ReleaseSmall -fstrip -fsingle-threaded -target x86_64-macos
$ ./main
Hello, World!
$ zig build-exe ./main.zig -O ReleaseSmall -fstrip -fsingle-threaded -target x86_64-windows-gnu
$ ls
main.zig main.exe
Terminal window
$ cat hello.c
#include <stdio.h>
int main(int argc, char **argv) {
fprintf(stderr, "Hello, World!\n");
return 0;
}
Terminal window
$ zig build-exe ./hello.c -O ReleaseSmall -fstrip
$ ./hello
Hello, World!

连 1.0 版本都没有,Uber 为什么会采用这样一项新技术? Uber 使用 Zig 来 编译其 C/C++代码,与其他工具链相比,zig-cc 提供的 C/C++工具链的主要优势是:glibc 版本可配制与 macOS 交叉编译。但 Uber 没有任何使用 zig-the-language 的计划。

  1. make self-hosted the default compiler #12368
  2. How Uber Uses Zig
  3. Bazel C/C++ toolchain for cross-compiling C/C++ programs
  4. Zig Language Reference
  5. ziglearn.org
  6. Mastering project managment in Zig

SSH端口转发

安全外壳协议(Secure Shell Protocol,简称SSH)是一种加密的网络传输协议,可在不安全的网络中为网络服务提供安全的传输环境。SSH通过在网络中创建安全隧道来实现SSH客户端与服务器之间的连接。SSH最常见的用途是远程登录系统,人们通常利用SSH来传输命令行界面和远程执行命令。

以前网络主机之间的通信是采用明文传输(如:Telnet和Berkeley rlogin、rsh、rexec等协议),使用不可靠的密码,容易遭受监听、嗅探和中间人攻击,SSH旨在保证非安全网络环境(例如互联网)中信息加密完整可靠。

SSH以非对称加密实现身份验证。身份验证有多种途径,例如其中一种方法是使用自动生成的公钥-私钥对来简单地加密网络连接,随后使用密码认证进行登录;另一种方法是人工生成一对公钥和私钥,通过生成的密钥进行认证,这样就可以在不输入密码的情况下登录。 任何人都可以自行生成密钥。公钥需要放在待访问的电脑之中,而对应的私钥需要由用户自行保管。认证过程基于生成出来的私钥,但整个认证过程中私钥本身不会传输到网络中。

SSH 的经典用途是登陆到远程电脑中执行命令,此外还支持隧道协议、端口映射和X11连接。借助SFTP或SCP协议,SSH还可以传输文件。 SSH 使用客户端-服务器模型, 标段端口为22。

SSH 作为加密通信的中介,充当两台服务器之间的通信加密跳板,使得原本不加密的通信变成加密通信。这个功能称为端口转发(port forwarding),又称 SSH 隧道(tunnel)。

端口转发有两个主要作用:

(1)将不加密的数据放在 SSH 安全连接里面传输,使得原本不安全的网络服务增加了安全性,比如通过端口转发访问 Telnet、FTP 等明文服务,数据传输就都会加密。

(2)作为数据通信的加密跳板,绕过网络防火墙。

端口转发有三种使用方法:动态转发本地转发远程转发

下面的场景中,假设HostA 和 HostB 之间通过防火墙分为两个部分,只能在HostA 和 HostB 之间通过SSH连接的方式来相互访问。

此时可以通过本地转发,既将SSH服务器作为中介跳板机,建立本地计算机与特定目标之间的加密连接。

它会指定一个本地端口(local-post),所有发向这个端口的请求,都会转发到SSH跳板机(tunnel-host),然后 SSH 跳板机作为中介,将收到的请求发到目标服务器(target-host)的目标端口(target-port)。

Terminal window
$ ssh -CNfgL local-port:target-host:target-port tunnel-host

上面命令中,-L参数表示本地转发,local-port是本地端口,target-host是你想要访问的目标服务器,target-port是目标服务器的端口,tunnel-host是 SSH 跳板机。

可选参数CNfg:
-C:压缩数据传输,
-N:不执行脚本或命令,仅进行端口转发
-f:ssh客户端在后台运行
-g:允许远程主机连接到建立的转发的端口,如果不加这个参数,只允许本地主机建立连接

场景1:HostA 节点访问 HostB 节点在PortB端口提供的TCP服务

1

那么可以通过下面的命令来实现:

(HostA) $ ssh -L PortA:HostB:PortB user@HostB

场景2 和场景1类似,但这次是想通过HostB 访问 HostC上PortC 端口提供的TCP服务

2

那么可以通过下面的命令来实现:

Terminal window
(HostA) $ ssh -L PortA:HostC:PortC user@HostB

远程转发指的是在远程 SSH 服务器建立的转发规则。

它跟本地转发正好反过来。建立本地计算机到远程计算机的 SSH 隧道以后,本地转发是通过本地计算机访问远程计算机,而远程转发则是通过远程计算机访问本地计算机。它的命令格式如下。

Terminal window
$ ssh -R remote-port:target-host:target-port -N remotehost

上面命令中,-R参数表示远程端口转发,remote-port是远程计算机的端口,target-host和target-port是目标服务器及其端口,remotehost是远程计算机。 它会指定一个远程端口(remote-port),所有发向那个端口的请求,都会转发到 SSH 跳板机(tunnel-host),然后 SSH 跳板机作为中介,将收到的请求发到目标服务器(target-host)的目标端口(target-port)。

场景3: 内网HostA 在PortA 端口有一个服务,可以通过远程转发将PortA端口,映射到外网HostB 的PortB 端口上,使得访问HostB 的PortB 端口,都会 转发到内网HostA 的PortA 端口

3

Terminal window
(HostA) ssh -R PortB:HostA:PortA user@HostB

动态转发指的是,本机与 SSH 服务器之间创建了一个加密连接,然后本机内部针对某个端口的通信,都通过这个加密连接转发。它的一个使用场景就是,访问所有外部网站,都通过 SSH 转发。

动态转发需要把本地端口绑定到 SSH 服务器。至于 SSH 服务器要去访问哪一个网站,完全是动态的,取决于原始通信,所以叫做动态转发。

Terminal window
$ ssh -D local-port tunnel-host -N

上面命令中,-D表示动态转发,local-port是本地端口,tunnel-host是 SSH 服务器,-N表示这个 SSH 连接只进行端口转发,不登录远程 Shell,不能执行远程命令,只能充当隧道。

注意,这种转发采用了 SOCKS5 协议。访问外部网站时,需要把 HTTP 请求转成 SOCKS5 协议,才能把本地端口的请求转发出去。

如下,在本地端口7891作为进行动态转发,在curl通过这个端口进行外部端口访问:

Terminal window
$ ssh -ND 7891 tunnel-host
$ curl -x socks5://localhost:7891 http://www.baidu.com

上面命令中,curl 的-x参数指定代理服务器,即通过 SOCKS5 协议的本地2121端口,访问http://www.baidu.com。此时 也可将浏览器的代理服务器设置成socks5://localhost:7891,访问外部网站。

  1. 维基百科Secure Shell
  2. 百度百科安全外壳协议
  3. SSH
  4. 什么是SSH 以及常见的ssh 功能
  5. 通过 SSH 实现 TCP / IP 隧道(端口转发):使用 OpenSSH 可能的 8 种场景