在duo上部署WebAssembly运行环境及udp点灯实例

在milk-v duo上部署wasm运行环境及udp点灯实例

一、废话前言

wasm,也就是WebAssembly,是一帮搞web前端的在2015年卷出来的鬼东西。
在web上,本来是html、javascript解释性语言的天下,手写几行代码就能玩出花来。但搞web前端的非要往死里卷,搞工程化,弄个东西要下载几个G的工具包。东西越来越大,性能咋办?继续卷,编译,编译了再到web环境运行,就搞出了WebAssembly。光把javascript给编译了还不够,继续卷,把c、c++的代码,甚至把python、rust等的代码,都能给编译了在web环境运行,连opencv都弄了opencv.js。

就像javascript光在前端运行还不够,还要继续卷一个nodejs,前后通吃。
同样的,WebAssembly不停地卷,居然卷到嵌入式的领域了,而且,居然还有不少好处:

  1. 因为是编译后的,运行速度非常好,和解释性语言相比那不是一条街的。
  2. 一次编译,到处运行,运行环境还超级小。
  3. wasm是在沙盒中运行的,对于安全性要求高的场合,非常适用。

java也能做到一次编译到处运行,但相对而言,java那是空天航母,wasm则是独木舟。

不继续废话了,下面来实际的,从基础环境,到在milk-v duo上面,跑起来一个udp点灯实例。

二、开发和运行环境

要在milk-v duo上面运行wasm,需要如下的开发和运行环境:

  1. 基础编译环境:host-tools,milk-v duo 本身的编译工具链,duo开发必备
  2. wasm编译环境:wasi-sdk,基于Clang & LLVM的 C/C++ 编译为 WASM工具链
  3. wasm的运行环境:WAMR,WebAssembly Micro Runtime,一个轻量级的独立wasm运行环境,具有占用空间小、高性能和高度可配置的功能,适用于从嵌入式、物联网、边缘到可信执行环境 (TEE)等。

基本的工作流程为:

  1. 用host-tools编译WAMR,生成在milk-v duo上面的运行环境。如果是在其他硬件环境上运行,需要为其他环境编译WAMR。
  2. 用wasi-sdk编译 C/C++ 程序为wasm程序,在 milk-v duo上面 运行。这个是一次编译,就可以在不同的硬件环境上运行。

各工具包的下载地址:

  1. host-tools
    host-tools下载号以后,将其路径添加到PATH中,确保如下的命令可以执行:
riscv64-unknown-linux-musl-gcc --version
  1. wasi-sdk
    wasi-sdk提供了多个环境的预编译包,方便在不同的环境上进行开发工作:

我日常使用macOS进行开发,所以我下载的是 wasi-sdk-20.0-macos.tar.gz,下载解压,搜一个软链接到/opt/wasi-sdk备用:

ln -s /Users/HonestQiao/Project/wasm/wasi-sdk-20.0 /opt/wasi-sdk
ls -l /opt/wasi-sdk/bin/

设置正确的情况下,将输出clang & llvm的工具链文件

  1. WAMR
    WAMR源码,直接使用git克隆即可:
git clone https://github.com/bytecodealliance/wasm-micro-runtime.git

在 wasm-micro-runtime/product-mini/platforms/linux 目录中,提供了克在linux运行的最小wasm环境。
可以直接编译当前普通linux环境的wamr运行环境,但要在milk-v duo上运行,则需要进行一些配置:
在 wasm-micro-runtime/product-mini/platforms/linux/CMakeLists.txt中的set (WAMR_BUILD_PLATFORM "linux")后添加配置,具体如下:

set (WAMR_BUILD_PLATFORM "linux")

+SET(CMAKE_C_COMPILER riscv64-unknown-linux-musl-gcc)
+SET(CMAKE_AR riscv64-unknown-linux-musl-ar)
+SET(CMAKE_RANLIB riscv64-unknown-linux-musl-ranlib)
+SET(CMAKE_STRIP riscv64-unknown-linux-musl-strip)
+SET(WAMR_BUILD_TARGET "RISCV64_LP64D")

+SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64")
+SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -mcpu=c906fdv -march=rv64imafdcv0p7xthead -mcmodel=medany -mabi=lp64d")

上述配置中,**+**表示添加的配置。

添加完成后,进行编译:

cd wasm-micro-runtime/product-mini/platforms/linux/
cmake .
make

ls -lh iwasm libiwasm.so

最终生成的iwasm,就是运行环境的入口,可以通过拷贝到(scp)milk-v duo上面/usr/bin/iwasm,然后运行:

iwasm --version

正常输出版本号 iwasm 1.2.3 说明iwasm运行部署环境成功。

然后,就可以写一个hello进行测试了。

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    printf("Hello world!\n");
    printf("I am wasm!\n");
    return 0;
}
  1. hello
    此时编译,需要使用wasi-sdk来进行编译,具体执行的操作如下:
export PATH="/opt/wasi-sdk/bin:$PATH"
clang -O3 -o hello.wasm hello.c
ls -lh *.wasm

正常执行后,会看到编译结果为 hello.wasm

如果你为当前运行的电脑环境上,编译了wamr,那么现在就可以执行iwasm hello.wasm
或者,你也可以将该wasm文件,上传到milk-v duo上面再执行iwasm hello.wasm

iwasm hello.wasm

正常情况下,输出为:

Hello world!
I am wasm!

现在,开发和运行环境已经准备好了,我们可以继续wasm之旅了。

三、udp点灯实例

在开发板上,有一个LED,对应GPIO 440,我们可以通过直接操作 /sys/class/gpio 来控制器亮灭,在 /mnt/system/blink.sh 中有使用shell操作的实例。

我们需要使用vi先对 /mnt/system/blink.sh 做一些手脚:

#!/bin/sh

LED_GPIO=/sys/class/gpio/gpio440

if test -d $LED_GPIO; then
    echo "GPIO440 already exported"
else
    echo 440 > /sys/class/gpio/export
fi

echo out > $LED_GPIO/direction

+let count=10
-while true; do
+while [ $count -gt 0 ]; do
    echo 0 > $LED_GPIO/value
    sleep 0.5
    echo 1 > $LED_GPIO/value
    sleep 0.5
+    let count=count-1
done
+
+echo 0 > $LED_GPIO/value
+echo 440 > /sys/class/gpio/unexport

上述脚本中,+ 表示添加的配置,- 表示去掉的配置。

这样,在亮灭10次后,就会自动退出,不会对其他的操控GPIO 440的程序产生影响。
修改完成后,reboot一次使得其生效。

在 wasm-micro-runtime/samples/socket-api/wasm-src 中,有关于socket的例子程序,我们可以参考其中的 udp_server.c ,来编写可以控制LED的 udp_light.c,具体内容如下:

#include "socket_utils.h"

#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#ifdef __wasi__
#include <wasi_socket_ext.h>
#endif

#include <fcntl.h> //define O_WRONLY and O_RDONLY

#define MAX_CONNECTIONS_COUNT 10000

// LED 引脚
#define SYSFS_GPIO_EXPORT "/sys/class/gpio/export"
#define SYSFS_GPIO_UNEXPORT "/sys/class/gpio/unexport"
#define SYSFS_GPIO_RST_PIN_VAL "440"
#define SYSFS_GPIO_RST_DIR_TPL "/sys/class/gpio/gpio%s/direction"
#define SYSFS_GPIO_RST_DIR_VAL "OUT"
#define SYSFS_GPIO_RST_VAL_TPL "/sys/class/gpio/gpio%s/value"
#define SYSFS_GPIO_RST_VAL_H "1"
#define SYSFS_GPIO_RST_VAL_L "0"

static void
init_sockaddr_inet(struct sockaddr_in *addr)
{
    /* 0.0.0.0:1234 */
    addr->sin_family = AF_INET;
    addr->sin_port = htons(1234);
    addr->sin_addr.s_addr = htonl(INADDR_ANY);
}

int
main(int argc, char *argv[])
{
    int socket_fd = -1, af, fd;
    socklen_t addrlen = 0;
    struct sockaddr_storage addr = { 0 };
    char reply_message[50] = {0};
    unsigned connections = 0;
    char ip_string[64] = { 0 };
    char buffer[1024] = { 0 };

    // gpio变量
    char str_sysfs_gpio_rst_pin_val[5] = {0};
    char str_sysfs_gpio_rst_dir[64] = {0};
    char str_sysfs_gpio_rst_val[64] = {0};

    // 从命令行获取gpio编号
    if (argc > 1 && strlen(argv[1])<4) {
        snprintf(str_sysfs_gpio_rst_pin_val, 4, "%s", argv[1]);
        printf("[Server] Info: gpio is config:%s\n", str_sysfs_gpio_rst_pin_val);
    } else {
        snprintf(str_sysfs_gpio_rst_pin_val, 4, "%s", SYSFS_GPIO_RST_PIN_VAL);
        printf("[Server] Info: gpio is defalt:%s\n", str_sysfs_gpio_rst_pin_val);
    }

    // 设置gpio对应的direction和value文件
    snprintf(str_sysfs_gpio_rst_dir, 50, SYSFS_GPIO_RST_DIR_TPL, str_sysfs_gpio_rst_pin_val);
    snprintf(str_sysfs_gpio_rst_val, 50, SYSFS_GPIO_RST_VAL_TPL, str_sysfs_gpio_rst_pin_val);

    // 打开端口/sys/class/gpio# echo ??? > export
    fd = open(SYSFS_GPIO_EXPORT, O_WRONLY);
    if (fd == -1)
    {
        perror("export gpio error.\n");
        goto fail;
    } else {
        printf("[Server] OK: export gpio %s success.\n", str_sysfs_gpio_rst_pin_val);
    }
    write(fd, SYSFS_GPIO_RST_PIN_VAL, sizeof(SYSFS_GPIO_RST_PIN_VAL));
    close(fd);

    // 设置端口方向/sys/class/gpio/gpio???# echo out > direction
    fd = open(str_sysfs_gpio_rst_dir, O_WRONLY);
    if (fd == -1)
    {
        perror("direction set error.\n");
        goto fail;
    } else {
        printf("[Server] OK: direction set success.\n");
    }
    write(fd, SYSFS_GPIO_RST_DIR_VAL, sizeof(SYSFS_GPIO_RST_DIR_VAL));
    close(fd);

    // 输出复位信号: 拉高>100ns
    fd = open(str_sysfs_gpio_rst_val, O_RDWR);
    if (fd == -1)
    {
        perror("gpio init error.\n");
        goto fail;
    } else {
        printf("[Server] OK: gpio init success.\n");
    }
    write(fd, SYSFS_GPIO_RST_VAL_L, sizeof(SYSFS_GPIO_RST_VAL_L));

    af = AF_INET;
    addrlen = sizeof(struct sockaddr_in);
    init_sockaddr_inet((struct sockaddr_in *)&addr);

    printf("[Server] Create socket\n");
    socket_fd = socket(af, SOCK_DGRAM, 0);
    if (socket_fd < 0) {
        perror("Create socket failed");
        goto fail;
    }

    printf("[Server] Bind socket\n");
    if (bind(socket_fd, (struct sockaddr *)&addr, addrlen) < 0) {
        perror("Bind failed");
        goto fail;
    }

    printf("[Server] Wait for clients to connect(max is %d) ..\n", MAX_CONNECTIONS_COUNT);
    while (connections < MAX_CONNECTIONS_COUNT) {
        addrlen = sizeof(addr);
        /* make sure there is space for the string terminator */
        int ret = recvfrom(socket_fd, buffer, sizeof(buffer) - 1, 0,
                           (struct sockaddr *)&addr, &addrlen);
        if (ret < 0) {
            perror("Read failed");
            goto fail;
        }
        buffer[ret] = '\0';

        if (sockaddr_to_string((struct sockaddr *)&addr, ip_string,
                               sizeof(ip_string) / sizeof(ip_string[0]))
            != 0) {
            printf("[Server] failed to parse client address\n");
            goto fail;
        }

        printf("[Server] received %d bytes from %s: %s\n", ret, ip_string,
               buffer);


        if(strcmp(buffer, "on")==0 || strcmp(buffer, "ON")==0) {
            snprintf(reply_message, 50, "light on");
            write(fd, SYSFS_GPIO_RST_VAL_H, sizeof(SYSFS_GPIO_RST_VAL_H));
        }
        else if(strcmp(buffer, "off")==0 || strcmp(buffer, "OFF")==0) {
            snprintf(reply_message, 50, "light off");
            write(fd, SYSFS_GPIO_RST_VAL_L, sizeof(SYSFS_GPIO_RST_VAL_L));
        }
        else if(strcmp(buffer, "q")==0 || strcmp(buffer, "quit")==0) {
            snprintf(reply_message, 50, "quit");
            break;
        }
        else {
            snprintf(reply_message, 50, "unknow command");
        }
        printf("[Server] Info: %s\n", reply_message);

        if (sendto(socket_fd, reply_message, strlen(reply_message), 0,
                   (struct sockaddr *)&addr, addrlen)
            < 0) {
            perror("Send failed");
            break;
        }

        connections++;
    }

    if (connections == MAX_CONNECTIONS_COUNT) {
        printf("[Server] Achieve maximum amount of connections\n");
    }

    close(fd);

    // 关闭端口/sys/class/gpio# echo ??? > unexport
    fd = open(SYSFS_GPIO_UNEXPORT, O_WRONLY);
    if (fd == -1)
    {
        perror("unexport gpio error.\n");
        goto fail;
    } else {
        printf("[Server] OK: unexport gpio %s success.\n", str_sysfs_gpio_rst_pin_val);
    }
    write(fd, SYSFS_GPIO_RST_PIN_VAL, sizeof(SYSFS_GPIO_RST_PIN_VAL));
    close(fd);

    printf("[Server] Shuting down ..\n");
    shutdown(socket_fd, SHUT_RDWR);
    close(socket_fd);
    sleep(3);
    printf("[Server] BYE \n");
    return EXIT_SUCCESS;

fail:
    printf("[Server] Shuting down ..\n");
    if (socket_fd >= 0)
        close(socket_fd);
    if (fd >= 0)
        close(fd);
    sleep(3);
    return EXIT_FAILURE;
}

在上述代码中,基于 udp_server.c 和 c 控制 GPIO,实现了 通过UDP发送on、off、quit到milk-v duo,实现点灯、熄灯、退出。

然后,添加相关的编译配置:

# 文件:samples/socket-api/CMakeLists.txt
 add_executable(udp_server ${CMAKE_CURRENT_SOURCE_DIR}/wasm-src/udp_server.c)
+add_executable(udp_light ${CMAKE_CURRENT_SOURCE_DIR}/wasm-src/udp_light.c)

# 文件:samples/socket-api/wasm-src/CMakeLists.txt
 compile_with_clang(udp_server.c)
+compile_with_clang(udp_light.c)

再进行编译:

cd wasm-micro-runtime/samples/socket-api
mkdir build
cd build
cmake ..
make

ls -l *.wasm

正常执行的情况下,将会输出这个实例中,所有生成的wasm程序。
将其中的 udp_light.wasm 上传到milk-v duo备用。

因为此处的 udp_light 使用了网络功能,而之前的 wasm-micro-runtime/product-mini/platforms/linux 最小环境,没有提供网络功能,因此需要添加配置:

# 文件:wasm-micro-runtime/product-mini/platforms/linux/CMakeLists.txt
+set(WAMR_BUILD_INTERP 1)
+set(WAMR_BUILD_FAST_INTERP 1)
+set(WAMR_BUILD_LIB_PTHREAD 1)

set (WAMR_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../..)

上述脚本中,+ 表示添加的配置。
然后重新编译,将编译后的iwasm上传到milk-v duo的/usr/bin/下备用。

现在,wamr环境运行入口iwasm有了,wasm程序udp_light.wasm也有了,可以在milk-v duo执行了:

iwasm --addr-pool=0.0.0.0/15 --dir=/sys/ udp_light.wasm 440

运行后,具体输出如下:

上述命令最后的440,表示板载的LED对应的GPIO。
另外,–addr-pool规定了可以使用的网络,–dir规定了可以操作的目录。

此时,可以在电脑上,使用网络工具,发送udp包给milk-v duo。我使用的是跨平台的comtool:

在milk-v duo上,则会收到消息,执行对应的动作:

注意接收on、off时,milk-v duo上的LED将会根据指令亮灭。

现在,你可以使用 c/c++ 编写你的程序,使用wasi-sdk编译为wasm,然后部署到milk-v duo上面运行了。编译出来的wasm程序,能直接放到其他部署了iwasm的硬件上,也可以运行了,也就是说,如果不是关联到特定硬件的功能,它就是通用的。

四、常见问题

  • 运行iwasm时,提示:permission denied
    • 通常原因为没有设置运行权限,使用命令添加运行权限:chmod +x iwasm
  • 运行iwasm时,提示:not found
    • 通常原因为,没有添加静态编译参数;注意在前一部分讲解中,第一次编译wamr时,需要进行配置。
    • 其他程序类似情况,也可能没有上传需要的库
  • 使用iwasm运行udp_light时时,提示:WASM module load failed: integer too large
    • 没有添加网络功能配置。注意在前一部分讲解中,第二编译wamr时,网络功能部分,需要添加配置,并重新编译运行环境,并上传
  • 使用iwasm运行udp_light时时,提示:export gpio error 或者 direction set error
    • 说明 --dir 没有正确设置,wasm程序无权操作
  • 使用iwasm运行udp_light时时,提示:Bind failed: Permission denied
    • 说明 --addr-pool 没有正确设置,wasm程序无权使用该网络

大佬666,又get到了新姿势,紫薯布丁。

乔楚老师的文章写得都很用心,感谢~~~~~~