分类目录归档:笔记

Windows配置网卡开启路由转发

有时候有的设备需要爬墙但又不能指定代理,而且家里又没有单独的可以爬墙的路由器时,可以用这个方法把windows系统的电脑临时变成网关来使用。

要用这个方法需要你的梯子软件开启TUN模式,才能把过来的网络流量转发到梯子去。注意,你可能需要用两张网卡来实现这个功能,否则会出现代理流量在clash虚拟网卡和出口网卡之间回环路由,一张只接受网络中的流量(在这上面配置路由转发),另一张作为网络出口。

此方法适用于windows 10以上的系统,在PowerShell(管理员)中使用以下命令即可在指定网卡上开启路由转发,开启后此网卡上接收到的网络流量会走系统路由表转发出去。

Set-NetIPInterface -InterfaceAlias "网卡接口名称" -AddressFamily IPv4 -Forwarding Enabled
#不指定网卡时将会开启所有网卡的路由转发
Set-NetIPInterface -AddressFamily IPv4 -Forwarding Enabled

使用以下命令查看网卡是否已经打开了路由转发:

Get-NetIPInterface -AddressFamily IPv4 | Format-Table InterfaceAlias, InterfaceIndex, Forwarding -AutoSize

注意此方法在重启系统后会失效,如果需要保持转发效果,可以写一个powershell脚本配合任务计划在开机后自动执行。

[PVE]在Proxmox Virtual Environment中给虚拟机直通PCIE SATA控制器

我在PVE中跑了个TrueNAS系统,不过之前一直是通过软SCSI控制器把硬盘一块块映射到虚拟机里面去的,虽然这样能用,但这样一个问题是有性能开销,truenas也不能直接对硬盘进行最直接的控制,另一个问题是换盘的时候都要重新执行映射指令,很麻烦。

于是我想着还是得实现真正的直通才行,要实现对虚拟机来说能让硬盘即插即用最干脆的方法还是直接把整个SATA控制器映射进去,我刚好有个插在主板m.2插槽上的PCIE转SATA扩展卡,扩出来5个SATA口。

注意主板上的SATA控制器是不可以映射到虚拟机里的,一个原因是其iommu分组一定会绑定主板芯片组里其它兼职的部分无法分开,另一个原因是如果你的PVE系统盘在主板上的SATA接口上或者在通过芯片组的m.2接口上(通常是远离CPU的那个m.2接口),会导致pve启动不了。

这篇文章的主要部分就是以上我踩的坑,分享出来给同样遇到问题的人参考一下,下面是跟着AI的指导操作的步骤。

编辑PVE的配置文件,设置在PVE主机中屏蔽需要直通硬件的驱动:

  1. 编辑 /etc/default/grub,在 GRUB_CMDLINE_LINUX_DEFAULT 中添加 intel_iommu=on(Intel)或 amd_iommu=on(AMD)以及 iommu=pt
  2. 如果你发现扩展卡和别的设备被捆绑在一个组里,可以添加 pcie_acs_override=downstream,multifunction
  3. 我这个AMD平台的示例:
    GRUB_CMDLINE_LINUX_DEFAULT="quiet amd_iommu=on iommu=pt pcie_acs_override=downstream,multifunction
  4. 编辑 /etc/modules,确保里面包含:
    vfio
    vfio_iommu_type1
    vfio_pci
    vfio_virqfd
  5. 执行proxmox-boot-tool refresh,然后重启PVE。
  6. 重启后执行以下命令检查 IOMMU 分组
    for d in /sys/kernel/iommu_groups/*/devices/*; do n=${d#*/iommu_groups/*}; n=${n%%/*}; printf '%s\t%s\n' "$n" "$(lspci -nns ${d##*/})"; done | sort -n

    如果此时你要直通的设备还是和其他设备共用一个组号,那么要考虑更换直通CPU的插槽,换插槽也没用就是没有办法了。

  7. 在刚才命令执行后你还会获得一个PCI地址,就在你设备所在行开头的那一串,类似:26:00.0,然后就要执行lspci -n -s 26:00.0查看你的设备ID(这里面的PCI地址替换成你自己设备的那个),我的设备id返回的是这个样式的:197b:0585
  8. 接下来要用vfio驱动接管你的设备,防止设备被ahci驱动接管。
    1. /etc/modprobe.d/vfio.conf中添加以下内容:options vfio-pci ids=你的设备ID
    2. /etc/modprobe.d/pve-blacklist.conf中添加:softdep ahci pre: vfio-pci
    3. 执行update-initramfs -u -k all
    4. 重启 PVE,重启后执行lspci -k -s 你的设备PCI地址,检查是否有Kernel driver in use: vfio-pci,有的话就是驱动成功被接管了。
  9. 然后就可以在虚拟机设置里添加PCIE设备了,编辑PCIE设备时选择“原始设备”,在里面选择你的SATA控制器,“所有功能”的选项不用勾,然后勾上”Rom-Bar”和”PCI-Express”保存即可。

完成以上步骤直通SATA控制器就设置好了,然后可以打开虚拟机。

[PostgreSQL]pgroonga扩展关闭强制前缀搜索

被这个破问题困扰了好几天,明明我没有在搜索语法中使用前缀搜索,但搜索结果就硬是会出现只有前缀匹配的结果,自己的测试结果和groonga文档里的结果都不一样,搞得自己怀疑是不是我的pgsql服务坏了。

挣扎了三天才发现pgroonga扩展打开了强制前缀搜索,要修改这个参数:

SET pgroonga.match_escalation_threshold = -1;

这个参数默认值为0,表示“如果主关键字的命中数为 0,则升级为松散前缀搜索”,设置为-1之后即可关闭这个功能。

 

 

PostgreSQL Docker从17版本升级到18版本

PostgreSQL Docker从17升级到18版本,有一些大的变动,因此升级过程需要进行一些额外的操作。

首先,17版本默认是不开启数据检验和的,因此需要先把17版本的数据库启用校验和,才能正确迁移升级到18版本。

  1. 开启校验和的第一步,一定要把数据库服务停止
  2. 然后执行:
    • docker run --rm -it \
        --mount "type=bind,src=17版本数据库目录路径,dst=/var/lib/postgresql/data" \
        postgres:17 \
        pg_checksums --enable --verbose --progress -D /var/lib/postgresql/data

      如果出现类似这样的错误:`pg_checksums: error: invalid segment number 0 in file name “/var/lib/postgresql/data/base/24699/pgrn.conf”`,可能是插件生成的文件此工具无法识别,暂时把它们移走,等执行完以上命令之后再移动回来。

  3. 完成后应该看到类似这样的结果提示:
    • Checksum operation completed
      Files scanned:   1991
      Blocks scanned:  7209262
      Files written:  1674
      Blocks written: 7209185
      pg_checksums: syncing data directory
      pg_checksums: updating control file
      Data checksum version: 1
      Checksums enabled in cluster

现在数据库的校验和就开启完成了,然后就是改变数据库的目录结构,17版本以及之前的数据目录在`/var/lib/postgresql/data`中,但18版本开始,数据目录需要在以大版本号命名的子目录中,使用以下流程进行迁移:

  1. 保持数据库服务处于停止的状态
  2. 首先你需要把原来的17版本数据库目录中的内容从`pgsql数据目录/data`中移动到`pgsql数据目录/17/docker`中(部分17版本已经把数据移动到了此目录中,视情况操作),也就是数据不再存放在”data”目录中,而是放在”版本号/docker”目录中。之后18版本的数据库将被生成在`pgsql数据目录/18/docker`目录中。
  3. 创建一个新的18版本空数据库,可以使用你自己的dockerfile或者compose文件创建,把17版本对应的配置文件复制一份改一下版本就行,以下是注意点:
    1. 注意使用”tianon/postgres-upgrade”工具需要确保新老版本的超级用户名一致,否则会报”FATAL: role “xxx” does not exist”的错误,而且我测试出来它没法通过环境变量指定新创建数据库的用户名和密码,只能使用”PGUSER”环境变量指定执行迁移时使用的用户,因此才有这一节独立创建空库的步骤,如果你原本数据库的超级用户是”postgres”,则可以尝试直接跳到下一个大步骤。
    2. 新建一个临时目录”pgsql数据目录/tmp”映射到将启动容器的”/var/lib/postgresql/”,创建完成后再把生成的”18″目录移动出来。(如果不使用临时目录,在创建新的容器时检测到数据目录中有其它包含数据库文件的目录它会拒绝启动)
    3. 创建空数据库示例:
        1. 确保你没有数据库容器在运行。
        2. 启动一个18版本的临时容器来创建数据库文件
          docker run --name temp-pg-18 \
            --mount "type=bind,src=pgsql数据目录/tmp,dst=/var/lib/postgresql/" \
            -e POSTGRES_USER=超级用户名 \
            -e POSTGRES_PASSWORD=超级用户密码 \
            -e POSTGRES_DB=postgres \
            -d postgres:18

          容器成功运行后等十几秒钟,检查”pgsql数据目录/tmp”目录中是否已经生成了数据库文件,有了的话执行命令清除临时容器:

          docker stop temp-pg-18 && docker rm temp-pg-18
        3. 最后把生成的目录从临时目录移出来(如果你刚刚已经手动创建了18目录,就把它先删除,再执行):
          mv tmp/18 ./18
          rm -r tmp
  4. 开始迁移,注意,如果你的17版本数据库使用了额外的扩展库,那么在迁移时你需要确保当前迁移环境中也有对应的扩展库,否则会提示”Your installation references loadable libraries that are missing from the new installation. You can add these libraries to the new installation, or remove the functions using them from the old installation.”。运行以下工具容器进行迁移:
    • docker run --rm \
        --mount "type=bind,src=pgsql数据目录,dst=/var/lib/postgresql" \
        --env "PGDATAOLD=/var/lib/postgresql/17/docker" \
        --env "PGDATANEW=/var/lib/postgresql/18/docker" \
        --env "PGUSER=超级用户名" \
        tianon/postgres-upgrade:17-to-18 \
          /bin/bash -c "apt update && \ #在这里执行你的依赖安装命令,如果不需要安装依赖的话可以把这个apt命令也去掉,仅占位示例用
          /usr/local/bin/docker-upgrade --link"

      不算安装依赖的时间,迁移过程应该很快会完成,并且给出如下提示:

      Upgrade Complete
      ----------------
      Some statistics are not transferred by pg_upgrade.
      Once you start the new server, consider running these two commands:
          /usr/lib/postgresql/18/bin/vacuumdb --all --analyze-in-stages --missing-stats-only
          /usr/lib/postgresql/18/bin/vacuumdb --all --analyze-only
      Running this script will delete the old cluster's data files:
          ./delete_old_cluster.sh
  5. 迁移完成之后使用18版本的数据库容器检查迁移状态:
    • 启动18版本服务之前:
      •  检查校验和(可选,如果是前面17版本刚刚生成的校验和则不用执行):
        docker run --rm -it \
          --mount "type=bind,src=pgsql数据目录,dst=/var/lib/postgresql/data" \
          postgres:18 \
          pg_checksums --check -D /var/lib/postgresql/data/18/docker
    • 启动18版本的服务
      • 重建统计信息,进入容器执行: vacuumdb -U 超级用户名 --all --analyze-in-stages

 

这样基本上就完成了迁移工作。

 

安卓应用脱壳逆向笔记

写一篇小笔记,记录一下第一次脱壳并逆向修改安卓应用的过程,为了避免麻烦,这里不会有被我逆向应用的信息。

为了把一个应用使用的一个库换成opencv,我这个连java都不会写的人硬着头皮消耗了一个月,甚至整个春节假期在做这个事情。

首先需要的一个工具是apktool [https://github.com/iBotPeaches/Apktool],它的作用是把应用安装包apk文件中的各个部分提取出来,变成一个apktool项目目录,且修改完之后我们也需要用它打包回apk。

以下是用ai生成的一个apktool反编译后目录的结构,另外此次逆向修改过程中也大量使用了ai,不然以我这第一次认识这些东西的知识储备,还得更久才能弄好。

your_app/
├── AndroidManifest.xml        # 应用主配置文件,声明组件、权限、包名等
├── apktool.yml                # APKTool 元数据,记录 SDK 版本、资源 ID 偏移、压缩规则等,用于重打包
├── smali/                     # 主 DEX 反编译出的 Dalvik 字节码(.smali),对应 Java/Kotlin 逻辑,如果没有选择反编译资源的话,则不会有这个目录,取而代之的是classes.dex文件
│   └── com/example/...        # 按原始包名组织的类文件
├── smali_classes2/            # (若存在)第二个 DEX 文件的 smali 代码,用于 multidex 应用
├── smali_classes3/            # (若存在)第三个 DEX,依此类推
├── res/                       # 编译后的资源目录,已还原为可读 XML
│   ├── layout/                # 界面布局文件(.xml)
│   ├── values/                # 字符串(strings.xml)、颜色(colors.xml)、样式(styles.xml)等
│   └── drawable-*/            # 图片资源,按屏幕密度分类(如 drawable-xhdpi)
├── assets/                    # (可选)原始资产目录,内容不被编译,通过 AssetManager 访问(如 .json、.db)
├── lib/                       # (可选)原生库目录,包含 .so 文件
│   ├── arm64-v8a/             # 64 位 ARM 架构原生库
│   └── armeabi-v7a/           # 32 位 ARM 架构原生库(其他架构如 x86 等也可能存在)
├── original/                  # (可选)原始 APK 中的 META-INF 目录,含签名文件(重打包时通常会被替换)
└── [resources.arsc]           # (注:此文件在反编译后不直接出现,其内容已解析并融入 res/ 目录中)

要修改应用的逻辑,则需要修改各个smali目录下的smali文件,这些是反编译出来的字节码文件。 继续阅读安卓应用脱壳逆向笔记

frida调试安卓应用时重载loadLibrary导致无法从应用库目录载入库文件

我让ai给我写了段调试安卓应用的frida脚本,里面有一段类似这样的代码:

var SystemClass = Java.use("java.lang.System");
SystemClass.loadLibrary.overload("java.lang.String").implementation = function (libname) {
	console.log("[库加载] System.loadLibrary被调用: " + libname);

	// 所有库都调用原始方法,不做任何拦截
	this.loadLibrary(libname);
};

然后调试应用的时候就发现应用总是只从”/system/lib/”和” /vendor/lib/”中寻找库文件了,结果当时是加载失败,踩了个坑,记录一下。

似乎一旦重载了这种方法,就会影响应用加载库的搜索路径,目前没找到什么好办法解决。

[ESPHOME]自定义开发

esphome提供的框架偏向于让我们尽量在yaml配置文件中定义逻辑,它提供了很多基本的功能,这样可以减少自己需要写的代码,也可以减少出问题的概率。

如果框架提供的部分满足不了自己的全部需求的话,就需要自己写c++代码。另外再强调一下,esphome基于platformIO工具链,再底层可以选Arduino,也可以是esp-idf,在开发时要考虑兼容性的话(比如在esp8266和esp32上都可以运行),就尽量使用esphome和arduino的api。

esphome提供了几种方式混入外部的代码:

  1. 在配置文件中使用”includes”选项直接一同编译指定的文件。
  2. 在配置文件中使用”libraries”选项引入外部的库。
  3. 使用外部组件,外部组件支持直接指定仓库地址,它在编译时会去把仓库克隆下来。当指定的是本地路径时,也就是把本地的一个目录当成一个组件库来处理。

要自定义代码我认为最好的方式就是在项目目录里定义一个本地的外部组件,虽然使用外部组件需要写一个python初始化脚本(这是我极其讨厌的,还好有AI帮我写),但这种组件的形式可以更容易地嵌入和调用我们的代码(因为如果你不用这种形式,就要禁用main.cpp自动生成,然后自己去手写入口文件,否则你的代码就没有地方可以调用。esphome框架默认会生成一份它自己的入口和loop,你就不能再在全局定义你的了。)。

继续阅读[ESPHOME]自定义开发

[ESPHOME]本地部署开发环境

这是一篇笔记,记录我开始尝试使用esphome做一些小工具的过程,之后可能还会有一些相关的笔记。
如果你要问为什么我不用home assistant里的esphome builder插件的话,原因是我需要插入我自己的项目代码,如果使用esphome builder 我就必须先把代码上传到某处然后把部件的地址写在配置文件里才能下载并编译,会很麻烦,所以需要一个本地开发环境用于快速测试更改。

安装环境

使用esphome主要是为了接入home assistant,esphome依赖platformIO(以下都简称为pio)作为底层框架,因此需要先安装它。在windows平台,我们可以直接使用vscode安装platformIO插件,这个插件将会帮我们把pio安装好,在安装pio的时候需要确保网络爬墙,否则大概率会安装失败,在实际使用时我们并不需要直接去用这个插件,之后的步骤都将靠esphome的命令完成。

然后是安装esphome工具链,这其实是一个python包,按照官网的方式安装:https://esphome.io/guides/installing_esphome

pip3 install wheel
pip3 install esphome

然后使用`esphome version`命令检查能否正确显示版本号即可。

项目初始化

接下来进行一个基础的固件编译测试,首先确定要使用的esp8266或者esp32的模块,然后用以下命令来初始化一个配置文件:

esphome wizard 配置文件名字.yaml

执行后将会引导你选择板子以及进行一些设置,这里就不赘述了,完成后会生成一个配置文件。

我在写这篇笔记时使用的测试板是一个esp8266的开发板,选择的是’nodemcuv2’。

固件编译

有了配置文件之后就可以使用以下命令来进行编译,编译出来的固件会在当前目录下的”.esphome\build”文件夹里,这里面还有其它的缓存之类的东西,如果之后的编译出现了奇怪的错误,可以尝试删除这个文件夹。

esphome compile 配置文件名字.yaml

固件烧录

编译好固件之后就可以用以下命令上传你的固件,第一次上传需要使用串口连接,以后的固件就可以通过网络OTA的方式进行更新了(也是这个命令,执行时会让你选择)。

esphome upload 配置文件名字.yaml

固件写完之后重启开发板,它将会开始连接到wifi,此时同一局域网里的home assistant应该就会提示发现了新的设备,问是否要添加,添加时把配置里的api密钥填进去连接即可。

额外测试

接下来配置一个简单的控制开关用来点亮开发板上的led,在配置文件里加入以下顶层项目:

# LED控制配置
output:
- platform: gpio
pin: D4
id: led_gpio
inverted: true #led在引脚低电平时是亮,所以要反转一下

switch:
- platform: output
name: "开发板LED"
output: led_gpio

然后再编译并上传,设备联网后在home assistant的该设备页面上应该会自动多出来一个控制开关,现在就可以通过这个开关控制led的亮灭了。


继续阅读[ESPHOME]本地部署开发环境

[ZFS]给Zpool中的硬盘附加镜像

刚刚在处理truenas里一块问题镜像盘的时候本想把它下线,结果不小心直接把它删除了,于是这个pool直接就被降级成了条带池。

然后我插进另一块盘之后想要再把它添加为镜像盘,Truenas的ui竟然不让,于是只能用命令,这里做个笔记。

首先用 zpool status 池名称 看一下想要镜像的盘在池中的id,这里我的是”df872c84-8689-44f1-8a1d-5f0ee673c34d”。

然后用以下命令直接把新的硬盘添加进池中即可。

zpool attach 池名称 df872c84-8689-44f1-8a1d-5f0ee673c34d /新盘的路径

 

[Zig]使用 std.Thread.Pool 线程池

我发现Zig的官方文档里似乎没有写这个`std.Thread.Pool`的具体用例,所以写一篇笔记给大家参考一下。

这个Zig标准库中的线程池主要用到两个部分,一个是线程池本身的结构体`std.Thread.Pool`,一个是用于等待任务结束的`std.Thread.WaitGroup`(等待组)结构体。等待组的作用是把一组相关的任务放在一起,之后可以使用特定方法等待它们全部完成。

流程:

  • 在线程池初始化好了之后在创建线程任务时给pool.spawnWg方法传入指定的WaitGroup和要执行的函数以及参数
  • 分配任务时线程池就会开始执行任务
  • →随时使用`pool.waitAndWork`方法阻塞当前线程,等待WaitGroup中所有任务完成
  • ↘或者在任何时候使用`wait_group.isDone()`方法检查任务是否全部完成,如果没有完成可以让主线程去做其他事情。

以下直接写一个示例,内容依然是在我前面的笔记中已经出现了好几次的多线程数据竞争示例。

const std = @import("std");
const debug = std.debug;
const Pool = std.Thread.Pool;
const Thread = std.Thread;
const WaitGroup = std.Thread.WaitGroup;

const print = debug.print;

const NUM_THREADS = 2;

var sum: u64 = 0;//让几个线程分别把这个sum自增,完成后sum应当由于非加锁自增导致其值为一个随机数
pub fn sumTask(idx: usize) void {
    const tid = Thread.getCurrentId(); // 获取线程真实的id
    print("Thread {} (idx:{}) : starting...\n", .{ tid, idx });
    var timer = std.time.Timer.start() catch unreachable; //获取一个计时器,用于之后计算线程执行时间
    const y = ∑
    for (0..50000000) |_| {
        y.* += 1;
    }
    print("Thread {} (idx:{}) : done in {}ms.\n", .{ tid, idx, timer.read() / 1000000 });
}
test "threadpool" {
    const tid = Thread.getCurrentId(); // 获取线程真实的id
    print("Main thread {} start\n", .{tid});
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();
    var thread_pool: Pool = undefined;
    try thread_pool.init(.{ //初始化线程池
        .allocator = allocator,
        .n_jobs = NUM_THREADS, //线程数
    });
    defer thread_pool.deinit();
    var wait_group: WaitGroup = undefined;
    wait_group.reset(); //初始化等待组
    //分配10个任务,线程池内的空闲线程会分别完成这些任务
    for (0..10) |idx| {
        thread_pool.spawnWg(
            &wait_group,//传入等待组
            sumTask,//传入任务函数
            .{idx},//任务函数的参数元组
        );
    }
    thread_pool.waitAndWork(&wait_group); //等待以上分配的任务全部完成
    print("Main: sum = {}\n", .{sum});
    print("Main: program completed. Exiting.\n", .{});
}

需要注意的是,如果在分配完任务后立刻使用`thread_pool.waitAndWork`等待任务完成(像上面的示例中一样,也就是在任务没有全部完成时就开始等待),你会发现虽然设置的线程数是2,理论上任务应该两个两个一起执行,但实际上却是3个任务一起执行的,因为调用这个等待方法的线程也执行了任务。我当时也疑惑了一会儿,然后想明白了,既然等待线程已经等在那里了,那么与其干等不如一起把任务做完,这样的逻辑确实是合理的,但这样实际并行执行的线程就会多一个,这是要注意的地方。

另一个要注意的是,目前的线程池实现中每一个线程启动之后就会开始从任务队列的开头取任务,但添加任务的时候也是添加到队列的开头的,所以虽然不严格保证顺序(因为你后续还可以继续添加任务),但任务队列大致上是后进先出的,所以不要连续不断地添加任务,至少要等线程池任务清空了才可以继续添加新的任务。(我觉得这样不太合理)

[WebAssembly]使用Zig语言制作wasm模块

“前几篇文章你不是还在写C++的wasm笔记,为什么突然开始使用Zig了?”

实际上由于C++古老的语言特性导致有的时候必须把声明和定义分开来写非常繁琐,而且在Emscripten环境下要添加第三方的库比较麻烦,所以我想改到一个自带包管理的语言。于是就首先捡起以前学到一半被劝退的Rust,这个国庆我花了几天时间把rust的教程看完了,但我又一次被这个反人类语言极致的复杂性劝退了,也就有了上一篇文章的内容。

在群友的推荐下我发现Zig这个类C的语言好像还行,于是抱着试一试的心态来学习一下,也就有了这篇笔记。不过不代表我之后就会用这个语言写项目,还有待观察。

这篇笔记的主要内容不是怎么编写Zig语言,而是如何把一个Zig项目编译成wasm模块,因此需要先有一点点的zig基础才能看懂。现在zig的文档和社区生态还不是很完善,且有的东西还在变化,有的东西是查不到或者很难查到的,我花了一些时间摸索才搞明白一些要点,这就是写这篇笔记的原因。

环境准备

如果还没有zig环境,可以用scoop快速部署,只需执行两个脚本即可

安装scoop

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression

用scoop安装最新版的zig,我写这篇笔记的时候zig版本是0.14.0-dev.1798,部分特性有可能在以后改变。

scoop bucket add versions
scoop install versions/zig-dev

以上即可完成zig环境的安装。顺带一提,使用scoop update zig-dev可以更新zig-dev的版本。

简单的示例

最简单快速的方式就是创建一个zig文件,然后使用命令把它编译成wasm文件。

main.zig 文件(由于代码高亮插件没有支持这个语言,而且以下示例看着挺像rust的,所以先用rust高亮来凑活一下)

const std = @import("std");

pub fn main() !void {
    const poi: u16 = 666;

    std.debug.print("poi: {}\n", .{poi});
}

然后使用命令编译

zig build-exe main.zig -target wasm32-wasi

此时目录下就会多出一个main.wasm和一个main.wasm.o文件(以后不同版本可能有所区别),和Emscripten不同的是它不会帮你生成一个胶水js文件供你直接加载。

这里我们使用的是wasi接口的wasm,如果你的运行环境是wasmtime的话,那么就可以直接执行这个wasm文件了。

生成的结果使用nodejs也可以运行,但要在nodejs中运行它,我们得自己为加载wasm写一些代码,还好也不多:

start.js 文件

const { readFile } = require('node:fs/promises');
const { WASI } = require('node:wasi');
const { argv, env } = require('node:process');
const { join } = require('node:path');

const wasi = new WASI({
	version: 'preview1',//该项必须定义,见文档:
	// https://nodejs.org/docs/latest-v20.x/api/wasi.html#new-wasioptions
	args: argv,//该项不是必须的,用于把node运行参数传给wasm程序
	env,//该项不是必须的,用传递环境变量
	
	//其它参数也见上面的文档链接
	//注意这是一个较新的特性,不同nodejs版本的接口可能会有区别,注意选择对应的文档版本
});

(async () => {
	const wasm = await WebAssembly.compile(
		await readFile(join(__dirname, 'main.wasm')),
	);
	const instance = await WebAssembly.instantiate(wasm, wasi.getImportObject());
	wasi.start(instance);
})();

使用nodejs执行以上js文件将得到输出结果:poi: 666

继续阅读[WebAssembly]使用Zig语言制作wasm模块

在SMB中创建符号链接

我直接使用命令在SMB卷上创建一个测试的链接,命令和返回的信息如下(注意该命令的/d选项需要使用管理员权限运行,否则会直接返回Access is denied,详见底部注意事项):

C:\Users\luojia> mklink /d "\\smb创建目标路径" "\\smb上被链接的目录"
You do not have sufficient privilege to perform this operation.

我已经使用管理员身份运行了,依然提示权限不足,那么就说明并不是我本机的权限不足,而是host端的权限问题了。进到SMB宿主机系统里,看了看共享权限和文件夹权限都已经全部给足了,问题也不在这里。

刚刚我查了一下,发现其实并不是SMB协议上没有能力创建符号链接,而是对于这个操作还有一个额外的选项对其进行权限控制。

修改方法很简单:

  1. 在“运行”里打开 secpol.msc
  2. 进入“本地策略”→“用户权限分配
  3. 双击“创建符号链接”会跳出一个属性框,默认应该有个管理员用户组,点击下面的“添加用户或组”添加你的SMB账户,或者如果你不介意的话也可以直接添加“Everyone”用户。
  4. 点击确定之后再到自己本机上测试,已经可以在SMB上创建符号链接了。

到这里应该已经完成了链接的创建,如果还有问题就接着看下文。

 

这里是一些mklink相关的注意事项和额外说明:

  • mklink的 /d 选项(需要管理员权限)创建的是符号链接,对象可以是目录或文件,它可以在本地使用绝对或相对路径创建链接。
  • mklink的 /j 选项创建的是重解析点(也叫目录连接点,仅用于目录),必须使用绝对路径。这个选项是无法在SMB上创建的,会提示 Local NTFS volumes are required to complete the operation.
  • mklink的 /H 选项可以创建同一个存储设备上文件的“硬链接”,即在硬盘上指向的是同一个文件,但你可以在不同的路径创建它的分身。这个选项不可以对目录使用,因为目录本身就是没有文件实体的。
  • 如果要创建SMB上的符号链接,不可以使用映射的驱动器路径,必须使用分享的UNC路径,如 “\\luojia-pc\samba分享\文件或目录”这样的形式,如果使用映射的驱动器路径,会提示“The system cannot find the path specified”或“系统找不到指定路径”。
  • Powershell里无法使用mklink命令,要先输入cmd回车切到cmd或者直接打开cmd才能用。Powershell有另一个New-Item命令可以用于创建符号链接。

如果你在创建符号链接的时候还遇到类似被禁用的提示,比如“The symbolic link cannot be followed because its type is disabled”,可以使用以下命令查看一下策略设定:

fsutil behavior query SymlinkEvaluation

我这里已经设置过了,所以显示的是

Local to local symbolic links are enabled.
Local to remote symbolic links are enabled.
Remote to local symbolic links are enabled.
Remote to remote symbolic links are enabled.

但默认并不是全部启用的,如果你不关心每个条目的意义,可以使用以下命令把它们全部开启

fsutil behavior set SymlinkEvaluation L2L:1
fsutil behavior set SymlinkEvaluation L2R:1
fsutil behavior set SymlinkEvaluation R2L:1
fsutil behavior set SymlinkEvaluation R2R:1

这些设置具体的含义可以参考微软官方文档:https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/fsutil-behavior

 

[WebAssembly]初学笔记 Pthreads多线程

如果有看不明白的地方请先看前置说明文章

对于有高性能要求的可并发任务来说,在程序一开始设计的时候就要把多线程协作考虑进去,因此介绍完了一些emscripten的基本操作之后,我立刻就来到了多线程的章节。

Emscripten提供了两种实现多线程的方式:

  • POSIX Threads (Pthreads) API (以下简称pthreads)(此工具中标准库的std::thread也基于pthreads实现)
  • Wasm Workers API (以下简称wasmworkers)(在这里继续再批判一次官方这破API文档竟然不写每个函数的用法,其它东西扯了一堆)

我参阅了一下wasmworkers的文档,以上两种API基本能实现相同的功能,区别在于它的wasmworkers是直接根据js原生worker原本的API实现的,因此输出的编译结果会比较小,而pthreads实现了完整的pthreads api,所以输出的编译结果会比较大。由于考虑程序的可移植性,所以这里我只学习使用pthreads实现多线程,如果对pthreads不熟悉的话可以看一看这个Rookie-Note上的学习文档简单了解一下,还是挺简单的,我也刚学会才来写了这篇笔记。

本篇笔记不会对pthreads api如何使用进行全面的说明,因为内容比较多且不是专属于wasm的内容,在代码编写上也和普通的 C pthread程序没有什么区别,可以看我上面贴的菜鸟笔记链接去学习,这里主要就说明一下emscripten中pthread的实现方式和举个例子,顺便贴上emscripten的pthread指导链接

首先要注意

  • 使用以上两种多线程API的时候都不可以和 `-sSINGLE_FILE` 编译参数搭配使用,也就是使用多线程就无法把输出文件全部打包到一个js文件里了。
  • 要使用pthreads需要浏览器开启SharedArrayBuffer支持(在目前的浏览器中由于存在安全原因默认是禁用的),在nodejs中可以直接使用。wasmworkers我没有测试,但应该也是一样的。
  • 编译参数:要使用pthreads库,需要在编译时添加 `-pthread` 参数。
  • 由于标准库中的std::thread在emscripten中也是基于pthread实现的,因此如果你比较熟悉标准库的`std::thread`也可以直接使用它,但要记得添加 `-pthread` 编译参数。

继续阅读[WebAssembly]初学笔记 Pthreads多线程

[WebAssembly]初学笔记 使用Embind在Javascript与C++之间交互

如果有看不明白的地方请先看前置说明文章

Embind是emscripten提供的又一种在js和c++之间的交互方案,其提供更加丰富的交互方式,不止是前面的笔记中介绍的那种简单的函数调用。

Embind库API参考文档地址:https://emscripten.org/docs/api_reference/bind.h.html

官方指导文档地址:https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html,下面简单概括一下这个库包含的功能。

1. 在js环境中绑定c++中的(绑定指的是把另一个语言中的概念映射为当前语言中类似的概念):

2. 在c++环境中:使用`val`类操作js中的任意对象。 继续阅读[WebAssembly]初学笔记 使用Embind在Javascript与C++之间交互

[WebAssembly]初学笔记 在C++中嵌入Javascript代码

如果有看不明白的地方请先看前置说明文章

前一篇笔记写了如何在js中调用c++的函数,要在c++里执行js代码也有几种方法,另外和从js到c++的交互只能调用函数和操作内存不同,c++里可以编写完整的js代码并获得结果。

这里依然先附上官方的参考:https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html


使用emscripten_run_script直接执行代码

这是emscripten提供的执行js代码的方法,此方法无法获取js代码的运行结果(类似于返回值),可以获取返回值的版本在下文。这个方案的内部实现方式是把这个代码字符串扔外面的js环境用eval()来执行。

在test.cpp中编写如下代码:

#include <emscripten.h>

int main() {
	emscripten_run_script(
		"console.log('岂因祸福避趋之');\n"
		"console.log('6');");
	return 0;
}

然后执行命令编译:emcc -sMODULARIZE=1 -sASSERTIONS -sEXPORT_ES6 -o dist/test.mjs test.cpp

继续阅读[WebAssembly]初学笔记 在C++中嵌入Javascript代码