机械境

这里只是我的后花园,随性而写

0%

背景

由于 WebAssembly Interface Types 还处于草案阶段,目前 WASM 只支持四种基本类型,本质上来说没什么大用。

在实际的开发中,数据结构肯定是远比 i32,i64,f32,f64 这四种基本类型来得复杂的,至少可能是一个复杂的结构体。比如,我需要把一组 key/value 存起来,然后可以通过 key 查询到对应的 value。按照目前的 WASM 的规范,这个实现都是比较麻烦的。key/value 可能是字符串或者其他结构,但是不管什么类型,在内存都是按照字节序的方式存储,所以只需要把类型编码成字节序放到内存中,这样这个结构就可以转化成内存的起始地址(指针)和数据长度,这两个值就是 i32/i64,并且都是 WASM 支持的类型。

函数声明

  • 对外 API

    为了对调用者友好,调用这不需要处理 slice 的地址

1
2
3
4
5
  // 把 key/value 写入存储
pub fn storage_write(key: &[u8], val: &[u8])

// 根据 key 查询对应的 value
pub fn storage_read(key: &[u8]) -> Option<Vec<u8>>
  • 内部接口

在虚拟机中注入这两个 host function 来实现具体的逻辑

1
2
3
4
5
6
7
8
9
10
extern "C" {
pub fn qlcchain_storage_read(
key: *const u8,
klen: u32,
val: *mut u8,
vlen: u32,
offset: u32,
) -> u32;
pub fn qlcchain_storage_write(key: *const u8, klen: u32, val: *const u8, vlen: u32);
}
Read more »

为了能够让人类阅读和编辑 WebAssembly,wasm 二进制格式提供了相应的文本表示。这是一种用来在文本编辑器、浏览器开发者工具等工具中显示的中间形式。本文用基本语法的方式解释了这种文本表示是如何工作的,以及它是如何与它表示的底层字节码,及在 JavaScript 中表示 wasm 的封装对象关联起来的。

本质上,这种文本形式更类似于处理器的汇编指令。

S - 表达式

不论是二进制还是文本格式,WebAssembly 代码中的基本单元是一个模块。在文本格式中,一个模块被表示为一个大的 S - 表达式。

S - 表达式是一个非常古老和非常简单的用来表示树的文本格式。因此,我们可以把一个模块想象为一棵由描述了模块结构和代码的节点组成的树。不过,与一门编程语言的抽象语法树不同的是,WebAssembly 的树是相当平的,也就是大部分包含了指令列表。

首先,让我们看下 S - 表达式长什么样。树上的每个一个节点都有一对括号——(…)——包围。括号内的第一个标签告诉你该节点的类型,其后跟随的是由空格分隔的属性或孩子节点列表。

S - 表达式如下:

1
(module (memory 1) (func))

这条表达式,表示一棵根节点为 “模块(module)” 的树,该树有两个孩子节点,分别是 属性为 1 的 “内存(memory)” 节点 和 一个 “函数(func)” 节点。

Read more »

WebAssembly 是一种运行在现代网络浏览器中的新型代码,并且提供新的性能特性和效果。它设计的目的不是为了手写代码而是为诸如 C、C++ 和 Rust 等低级源语言提供一个高效的编译目标。对于网络平台而言,这具有巨大的意义——为客户端 app 提供了一种在网络平台以接近本地速度的方式运行多种语言编写的代码的方式。JavaScript 框架不但可以使用 WebAssembly 获得巨大性能优势和新特性,而且还能使得各种功能保持对网络开发者的易用性。简而言之,WebAssembly 可以为 Javascript 提供由 C/C++ 等更高效的语言编写的库。

前世今生

问题

业务需求越来越复杂,前端的开发逻辑越来越复杂,相应的代码量随之变的越来越多,整个项目的起步的时间越来越长。在性能不好的电脑上,启动一个前端的项目甚至要花上十多秒。除了逻辑复杂、代码量大,还有另一个原因是 JavaScript 这门语言本身的缺陷,JavaScript 没有静态变量类型。这门解释型编程语言的作者 Brendan Eich,仓促的创造了这门如果被广泛使用的语言,以至于 JavaScript 的发展史甚至在某种层面上变成了填坑史。

asm.js

为了解决这个问题,WebAssembly 的前身,asm.js 诞生了。asm.js 是一个 Javascript 的严格子集,合理合法的 asm.js 代码一定是合理合法的 JavaScript 代码,但是反之就不成立。同 WebAssembly 一样,asm.js 不是用来给各位用手一行一行撸的代码,asm.js 是一个编译目标。它的可读性、可读性虽然比 WebAssembly 好,但是对于开发者来说,仍然是无法接受的。

无论 asm.js 对静态类型的问题做的再好,它始终逃不过要经过 Parser,要经过 ByteCode Compiler,而这两步是 JavaScript 代码在引擎执行过程当中消耗时间最多的两步。而 WebAssembly 不用经过这两步。这就是 WebAssembly 比 asm.js 更快的原因。

WebAssembly 横空出世

在 2015 年,我们迎来了 WebAssembly,是经过编译器编译之后的代码,体积小、起步快。在语法上完全脱离 JavaScript,同时具有沙盒化的执行环境。WebAssembly 同样的强制静态类型,是 C/C++/Rust 的编译目标。

2019 年 12 月 5 日 — 万维网联盟(W3C)宣布 WebAssembly 核心规范 成为正式标准

工具链

  • AssemblyScript
    支持直接将 TypeScript 编译成 WebAssembly
  • Emscripten
    将其 C/C++ 编译成 WebAssembly
  • WABT
    将 WebAssembly 在字节码和文本格式相互转换的一个工具,方便开发者去理解这个 wasm 到底是在做什么事。
  • WebAssembly Studio
    在线 IDE,支持 C/C++/Rust

关键概念

模块

表示一个已经被浏览器编译为可执行机器码的 WebAssembly 二进制代码。一个模块是无状态的,并且像一个二进制大对象(Blob)一样能够被缓存到 IndexedDB 中或者在 windows 和 workers 之间进行共享(通过 postMessage() 函数)。一个模块能够像一个 ES2015 的模块一样声明导入和导出。

内存

ArrayBuffer(浏览器 WASM 虚拟机实现),大小可变。本质上是连续的字节数组,WebAssembly 的低级内存存取指令可以对它进行读写操作。

表格

带类型数组,大小可变。表格中的项存储了不能作为原始字节存储在内存里的对象的引用(为了安全和可移植性的原因)。

实例

一个模块及其在运行时使用的所有状态,包括内存、表格和一系列导入值。一个实例就像一个已经被加载到一个拥有一组特定导入的特定的全局变量的 ES2015 模块。

生命周期

wasm lifecycle

  1. 通过工具链把其他语言 (C++、Go、 Rust) 编译成 WebAssembly 汇编格式 .wasm 文件
  2. 在网页中使用 fetchXMLHttpRequest 等获取 wasm 文件
  3. .wasm 编译为模块,编译过程中进行合法性检查
  4. 实例化,初始化导入对象,创建模块的实例
  5. 执行实例的导出函数,完成所需操作

虚拟机体系

wasm vm system

WebAssembly 模块在运行时由以下几部分组成,如上图所示

  1. 一个全局类型数,与很多语言不同,在 WebAssembly 中 “类型” 指的并非数据类型,而是函数签名,函数签名定义了函数的参数个数参数类型返回值类型; 某个函 数签名在类型数组中的下标 (或者说位置) 称为类型索引
  2. 一个全局函数数组,其中容纳了所有的函数,包括导入的函数以及模块内部定 义的函数,某个函数在函数数组中的下标称为函数索引
  3. 一个全局变量数组,其中容纳了所有的全局变量包括导入的全局变量以及 模块内部定义的全局变量,某个全局变量在全局变量数组的下标称为全局变量索引
  4. 一个全局表格对象,表格也是一个数组,其中存储了元素 (目前元素类型只能 为函数) 的引用,某个元素在表格中的下标称为元素索引。
  5. 一个全局内存对象
  6. 一个运行时栈
  7. 函数执行时可以访问一个局部变量数组,其中容纳了函数所有的局部变量,某 个局部变量在局部变量数组中的下标称为局部变量索引

在 WebAssembly 中,操作某个具体的对象(比如读写某个全局变量)都是通过其索引完成。在当前版本中,所有的索引都是 32 位整形数。

参考资料

---EOF---

介绍

之前在 traefik 基于 Treafik 部署 Bitwarden RS 中简单介绍了如何基于 Traefik 部署 Bitwarden RS。Treafik 的官方文档一如既往地混乱,甚至有些地方前后不一致,让人看得云里雾里的。

Traefik v2 的新特性

  • Providers
    是指你正在使用的集群技术 (Kubernetes, Docker, Consul, Mesos 等)
  • Entrypoints
    是最基本的配置,指监听请求的端口.
  • Services
    是在你的基础设施上的运行的软件,通过 services 配置如何路由到真正的应用程序
  • Routers
    将传入的请求和你的 service 连接起来. 他们持有的 rules 决定哪个 service 将处理对应的请求
  • Middleware
    中间件是可以在请求被 service 处理之前对其进行更新的组件。Traefik 提供了一些开箱即用的中间件来处理 认证、速率限制、断路器、白名单、缓冲等等

Traefik v2 配置

configuration

配置分为静态配置和动态配置:

  • 静态配置: 启动的时候加载。包括 Entrypoints、Provider 连接信息
  • 动态配置: 运行时可动态读取改变的。包括 Routes、Services、Middlewares、Certificates
Read more »

缘起

在 Dockerfile 中,如果我们不显式指明用户,进行权限处理,那么默认的 Dockerfile 生成的镜像,在运行时会以 root 身份进入 ENTRYPOINT,进而执行 CMD。因此,以 root 身份启动 container 是一件很危险的事情。尽管,docker 容器内的 root 与宿主 host 本身的 root 并不一定具有一样的权限,但是在容器内部的 root 拥有和宿主机一样的 UID(UID 0)。如果以 priviledge 的方式运行 container,那么两者将会一样,从而产生巨大的安全隐患。

简而言之,出于安全的考虑,需要在 Dockerfile 的 ENTRYPOINT 之前,也就是运行 application 之前,进行用户切换。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# Build gqlc in a stock Go builder container
FROM qlcchain/go-qlc-builder:latest as builder

ARG BUILD_ACT=build

COPY . /qlcchain/go-qlc
RUN cd /qlcchain/go-qlc && make clean ${BUILD_ACT}

# Pull gqlc into a second stage deploy alpine container
FROM alpine:3.11.3
LABEL maintainer="developers@qlink.mobi"

ENV QLCHOME /qlcchain

RUN apk --no-cache add ca-certificates && \
addgroup qlcchain && \
adduser -S -G qlcchain qlcchain -s /bin/sh -h "$QLCHOME" && \
chown -R qlcchain:qlcchain "$QLCHOME"

USER qlcchain

WORKDIR $QLCHOME

COPY --from=builder /qlcchain/go-qlc/build/gqlc /usr/local/bin/gqlc

EXPOSE 9734 9735 9736

ENTRYPOINT ["gqlc"]

VOLUME ["$QLCHOME"]

主要思路如下:

  • 一阶段构建需要执行的 app 可执行程序
  • 二阶段创建用户及用户组,拷贝一阶段的编译出来的文件
  • 切换到刚创建出来的用户,执行 ENTRYPOINTCMD

---EOF---

环境变量换行

1
2
3
4
# base64 编码
export PRIVATE_KEY=$(cat ~/private_key.gpg | base64)
# 用的时候,base64解码
echo -e "${PRIVATE_KEY}" | base64 -d >"$key_file"

常用别名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# ------------------------------------
# Docker alias and function
# ------------------------------------

# Get latest container ID
alias dl="docker ps -l -q"

# Get container process
alias dps="docker ps"

# Get process included stop container
alias dpa="docker ps -a"

# Get images
alias di="docker images"

# Get container IP
alias dip="docker inspect --format '{{ .NetworkSettings.IPAddress }}'"

# Run deamonized container, e.g., $dkd base /bin/echo hello
alias dkd="docker run -d -P"

# Run interactive container, e.g., $dki base /bin/bash
alias dki="docker run -i -t -P"

# Execute interactive container, e.g., $dex base /bin/bash
alias dex="docker exec -i -t"

# Stop all containers
dstop() { docker stop $(docker ps -a -q); }

# Remove all containers
drm() { docker rm $(docker ps -a -q); }

# Stop and Remove all containers
alias drmf='docker stop $(docker ps -a -q) && docker rm $(docker ps -a -q)'

# Remove all images
dri() { docker rmi $(docker images -q); }

# Dockerfile build, e.g., $dbu tcnksm/test
dbu() { docker build -t=$1 .; }

# Show all alias related docker
dalias() { alias | grep 'docker' | sed "s/^\([^=]*\)=\(.*\)/\1 => \2/"| sed "s/['|\']//g" | sort; }

# Bash into running container
dbash() { docker exec -it $(docker ps -aqf "name=$1") bash; }

---EOF---

缘起

之前一个项目做预研的时候搞了个树莓派 3B,测试完之后,一直处于吃灰状态。刚好最近经常给小朋友打印东西,家里那台老的 HP P1106 不支持无线打印,每次都接 USB 才能打印,让人无比烦躁。于是乎抽空把打印机接到树莓派上,利用 cups 做成无线打印。

安装系统

烧镜像

  • 官网 下载最新的镜像
  • 可以通过 dd 把镜像复制到 SD 卡中,具体方法参考 官方指南
  • 也可以通过 Etcherbrew cask install balenaetcher 即可,傻瓜式操作
Read more »

思路

cgo 交叉编译 中通过 xgo + Docker 实现 Cgo 的交叉编译。由于原版 xgo 不支持 go mod,而且作者貌似也没支持的打算,所有我自己维护了一个 xgo。使用了一段时间下来看,除了编译速度有点慢意外,其他还好。这也是跟我们的使用方式有关,我们需要根据不同的 tags 来编译不不同的版本,xgo 每次编译都要构建一个 Docker 实例,然后下载引用的库等等一大堆重复操作,导致整个编译时间拉长。

既然有很多重复操作,那就换个思路,只构建一个实例,把编译环境准备好,然后在这个实例中把要编译的内容,一次编译出来。goreleaser,支持编译发布 golang 的二进制文件,同时也支持发布 Docker 镜像,但是这不是重点,这里不再赘述。

实践

由于需要支持 macOS 就需要安装 OSX 的 SDK,在 xgo 是自己构建的基础镜像。这里通过 Docker 官方提供的 golang-cross 来作为基础镜像支持 macOS 平台,顺便规避一下版权的问题。在此基础上安装 mingw 支持 Windows x64/i386,以及 arm-gcc 等。剩下的就是一些常规操作了,比如安装 goreleaser,更新 golang 运行时等,完整的 Dockerfile

使用

1
2
3
4
5
docker run --rm --privileged \
-v $PWD:/go/src/github.com/qlcchain/go-qlc \
-v /var/run/docker.sock:/var/run/docker.sock \
-w /go/src/github.com/qlcchain/go-qlc \
goreng/golang-cross goreleaser --snapshot --rm-dist

当然也是可以添加到 makefile 中的

1
2
3
4
5
6
7
8
snapshot:
docker run --rm --privileged \
-v $(CURDIR):/go-qlc \
-v /var/run/docker.sock:/var/run/docker.sock \
-v $(GOPATH)/src:/go/src \
-w /go-qlc \
goreng/golang-cross:$(GO_BUILDER_VERSION) \
goreleaser --snapshot --rm-dist

使用的时候,只需要执行 make snapshot 即可。

小结

使用新的镜像之后,整体编译速度有了比较大幅度的提升,得益于 goreleaser 对打包的支持,原来自己实现 shell 打包生成 checksum 的操作,都有现成的支持。

---EOF---