Pārlūkot izejas kodu

add nn-cli example (#4373)

an example application with flexible cli options which
aims to allow us to perform any wasi-nn operations.

eg.
```
--load-graph=file=fixture/model.xml,file=fixture/model.bin,id=graph
--init-execution-context=graph-id=graph,id=ctx
--set-input=file=fixture/tensor.bgr,context-id=ctx,dim=1,dim=3,dim=224,dim=224
--compute=context-id=ctx
--get-output=context-id=ctx,file=output.bin
```
YAMAMOTO Takashi 6 mēneši atpakaļ
vecāks
revīzija
b3ce192e1a

+ 12 - 0
wamr-wasi-extensions/samples/nn-cli/CMakeLists.txt

@@ -0,0 +1,12 @@
+# Copyright (C) 2025 Midokura Japan KK.  All rights reserved.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+cmake_minimum_required(VERSION 3.14)
+
+set(CMAKE_C_STANDARD 99)
+set(CMAKE_C_STANDARD_REQUIRED YES)
+
+project(nn-cli LANGUAGES C)
+add_executable(nn-cli main.c fileio.c map.c)
+find_package(wamr-wasi-nn REQUIRED)
+target_link_libraries(nn-cli wamr-wasi-nn)

+ 73 - 0
wamr-wasi-extensions/samples/nn-cli/fileio.c

@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2025 Midokura Japan KK.  All rights reserved.
+ * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+ */
+
+/*
+ * modified copy-and-paste from:
+ * https://github.com/yamt/toywasm/blob/0eaad8cacd0cc7692946ff19b25994f106113be8/lib/fileio.c
+ */
+
+#include <sys/stat.h>
+
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "fileio.h"
+
+int
+map_file(const char *path, void **pp, size_t *sizep)
+{
+    void *p;
+    size_t size;
+    ssize_t ssz;
+    int fd;
+    int ret;
+
+    fd = open(path, O_RDONLY);
+    if (fd == -1) {
+        ret = errno;
+        assert(ret != 0);
+        return ret;
+    }
+    struct stat st;
+    ret = fstat(fd, &st);
+    if (ret == -1) {
+        ret = errno;
+        assert(ret != 0);
+        close(fd);
+        return ret;
+    }
+    size = st.st_size;
+    if (size > 0) {
+        p = malloc(size);
+    }
+    else {
+        /* Avoid a confusing error */
+        p = malloc(1);
+    }
+    if (p == NULL) {
+        close(fd);
+        return ENOMEM;
+    }
+    ssz = read(fd, p, size);
+    if (ssz != size) {
+        ret = errno;
+        assert(ret != 0);
+        close(fd);
+        return ret;
+    }
+    close(fd);
+    *pp = p;
+    *sizep = size;
+    return 0;
+}
+
+void
+unmap_file(void *p, size_t sz)
+{
+    free(p);
+}

+ 14 - 0
wamr-wasi-extensions/samples/nn-cli/fileio.h

@@ -0,0 +1,14 @@
+/*
+ * Copyright (C) 2025 Midokura Japan KK.  All rights reserved.
+ * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+ */
+
+/*
+ * modified copy-and-paste from:
+ * https://github.com/yamt/toywasm/blob/0eaad8cacd0cc7692946ff19b25994f106113be8/lib/fileio.h
+ */
+
+int
+map_file(const char *filename, void **pp, size_t *szp);
+void
+unmap_file(void *p, size_t sz);

+ 496 - 0
wamr-wasi-extensions/samples/nn-cli/main.c

@@ -0,0 +1,496 @@
+/*
+ * Copyright (C) 2025 Midokura Japan KK.  All rights reserved.
+ * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+ */
+
+#include <sys/stat.h>
+
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <wamr/wasi_ephemeral_nn.h>
+
+#include "fileio.h"
+#include "map.h"
+
+static struct map graphs;
+static struct map contexts;
+
+static void
+load_graph(char *options)
+{
+    int target = wasi_ephemeral_nn_target_cpu;
+    int encoding = wasi_ephemeral_nn_encoding_openvino;
+    const char *id = "default";
+    wasi_ephemeral_nn_graph_builder *builders = NULL;
+    size_t nbuilders = 0;
+    enum {
+        opt_id,
+        opt_file,
+        opt_encoding,
+        opt_target,
+    };
+    static char *const keylistp[] = {
+        [opt_id] = "id",
+        [opt_file] = "file",
+        [opt_encoding] = "encoding",
+        [opt_target] = "target",
+        NULL,
+    };
+    while (*options) {
+        extern char *suboptarg;
+        char *value;
+        const char *saved = options;
+        switch (getsubopt(&options, keylistp, &value)) {
+            case opt_id:
+                if (value == NULL) {
+                    fprintf(stderr, "no value for %s\n", saved);
+                    exit(2);
+                }
+                id = value;
+                break;
+            case opt_file:
+                if (value == NULL) {
+                    fprintf(stderr, "no value for %s\n", saved);
+                    exit(2);
+                }
+                builders =
+                    realloc(builders, (nbuilders + 1) * sizeof(*builders));
+                if (builders == NULL) {
+                    exit(1);
+                }
+                wasi_ephemeral_nn_graph_builder *b = &builders[nbuilders++];
+                int ret = map_file(value, (void *)&b->buf, (void *)&b->size);
+                if (ret != 0) {
+                    fprintf(stderr, "map_file \"%s\" failed: %s\n", value,
+                            strerror(ret));
+                    exit(1);
+                }
+                break;
+            case opt_encoding:
+                if (value == NULL) {
+                    fprintf(stderr, "no value for %s\n", saved);
+                    exit(2);
+                }
+                encoding = atoi(value);
+                break;
+            case opt_target:
+                if (value == NULL) {
+                    fprintf(stderr, "no value for %s\n", saved);
+                    exit(2);
+                }
+                target = atoi(value);
+                break;
+            case -1:
+                fprintf(stderr, "unknown subopt %s\n", saved);
+                exit(2);
+        }
+    }
+
+    wasi_ephemeral_nn_error nnret;
+    wasi_ephemeral_nn_graph g;
+    nnret = wasi_ephemeral_nn_load(builders, nbuilders, encoding, target, &g);
+    size_t i;
+    for (i = 0; i < nbuilders; i++) {
+        wasi_ephemeral_nn_graph_builder *b = &builders[i];
+        unmap_file(b->buf, b->size);
+    }
+    if (nnret != wasi_ephemeral_nn_error_success) {
+        fprintf(stderr, "load failed with %d\n", (int)nnret);
+        exit(1);
+    }
+    map_set(&graphs, id, g);
+}
+
+static void
+init_execution_context(char *options)
+{
+    const char *id = "default";
+    const char *graph_id = "default";
+    enum {
+        opt_id,
+        opt_graph_id,
+    };
+    static char *const keylistp[] = {
+        [opt_id] = "id",
+        [opt_graph_id] = "graph-id",
+        NULL,
+    };
+    while (*options) {
+        extern char *suboptarg;
+        char *value;
+        const char *saved = options;
+        switch (getsubopt(&options, keylistp, &value)) {
+            case opt_id:
+                if (value == NULL) {
+                    fprintf(stderr, "no value for %s\n", saved);
+                    exit(2);
+                }
+                id = value;
+                break;
+            case opt_graph_id:
+                if (value == NULL) {
+                    fprintf(stderr, "no value for %s\n", saved);
+                    exit(2);
+                }
+                graph_id = value;
+                break;
+            case -1:
+                fprintf(stderr, "unknown subopt %s\n", saved);
+                exit(2);
+        }
+    }
+
+    wasi_ephemeral_nn_graph g = map_get(&graphs, graph_id);
+    wasi_ephemeral_nn_graph_execution_context c;
+    wasi_ephemeral_nn_error nnret;
+    nnret = wasi_ephemeral_nn_init_execution_context(g, &c);
+    if (nnret != wasi_ephemeral_nn_error_success) {
+        fprintf(stderr, "init_execution_context failed with %d\n", (int)nnret);
+        exit(1);
+    }
+    map_set(&contexts, id, c);
+}
+
+static void
+set_input(char *options)
+{
+    int ret;
+    const char *context_id = "default";
+    uint32_t idx = 0;
+    wasi_ephemeral_nn_tensor tensor = {
+        .dimensions = { .buf = NULL, .size = 0, },
+        .type = wasi_ephemeral_nn_type_fp32,
+        .data = NULL,
+    };
+    void *buf = NULL;
+    size_t sz = 0;
+    enum {
+        opt_context_id,
+        opt_dim,
+        opt_type,
+        opt_idx,
+        opt_file,
+    };
+    static char *const keylistp[] = {
+        [opt_context_id] = "context-id",
+        [opt_dim] = "dim",
+        [opt_type] = "type",
+        [opt_idx] = "idx",
+        [opt_file] = "file",
+        NULL,
+    };
+    while (*options) {
+        extern char *suboptarg;
+        char *value;
+        const char *saved = options;
+        switch (getsubopt(&options, keylistp, &value)) {
+            case opt_context_id:
+                if (value == NULL) {
+                    fprintf(stderr, "no value for %s\n", saved);
+                    exit(2);
+                }
+                context_id = value;
+                break;
+            case opt_dim:
+                if (value == NULL) {
+                    fprintf(stderr, "no value for %s\n", saved);
+                    exit(2);
+                }
+                wasi_ephemeral_nn_tensor_dimensions *dims = &tensor.dimensions;
+
+                dims->buf =
+                    realloc(dims->buf, (dims->size + 1) * sizeof(*dims->buf));
+                if (dims->buf == NULL) {
+                    exit(1);
+                }
+                dims->buf[dims->size++] = atoi(value);
+                break;
+            case opt_type:
+                if (value == NULL) {
+                    fprintf(stderr, "no value for %s\n", saved);
+                    exit(2);
+                }
+                tensor.type = atoi(value);
+                break;
+            case opt_file:
+                if (value == NULL) {
+                    fprintf(stderr, "no value for %s\n", saved);
+                    exit(2);
+                }
+                if (buf != NULL) {
+                    fprintf(stderr, "duplicated tensor data\n");
+                    exit(2);
+                }
+                ret = map_file(value, &buf, &sz);
+                if (ret != 0) {
+                    fprintf(stderr, "map_file \"%s\" failed: %s\n", value,
+                            strerror(ret));
+                    exit(1);
+                }
+                break;
+            case opt_idx:
+                if (value == NULL) {
+                    fprintf(stderr, "no value for %s\n", saved);
+                    exit(2);
+                }
+                idx = atoi(value);
+                break;
+            case -1:
+                fprintf(stderr, "unknown subopt %s\n", saved);
+                exit(2);
+        }
+    }
+
+    if (tensor.dimensions.size == 0) {
+        fprintf(stderr, "no dimension is given\n");
+        exit(2);
+    }
+    if (buf == NULL) {
+        fprintf(stderr, "no tensor is given\n");
+        exit(2);
+    }
+
+    /*
+     * REVISIT: we can check the tensor size against type/dimensions
+     * and warn the user if unexpected.
+     */
+
+    wasi_ephemeral_nn_error nnret;
+    wasi_ephemeral_nn_graph_execution_context c =
+        map_get(&contexts, context_id);
+    tensor.data = buf;
+    nnret = wasi_ephemeral_nn_set_input(c, idx, &tensor);
+    unmap_file(buf, sz);
+    if (nnret != wasi_ephemeral_nn_error_success) {
+        fprintf(stderr, "set_input failed with %d\n", (int)nnret);
+        exit(1);
+    }
+}
+
+static void
+compute(char *options)
+{
+    const char *context_id = "default";
+    enum {
+        opt_context_id,
+    };
+    static char *const keylistp[] = {
+        [opt_context_id] = "context-id",
+        NULL,
+    };
+    while (*options) {
+        extern char *suboptarg;
+        char *value;
+        const char *saved = options;
+        switch (getsubopt(&options, keylistp, &value)) {
+            case opt_context_id:
+                if (value == NULL) {
+                    fprintf(stderr, "no value for %s\n", saved);
+                    exit(2);
+                }
+                context_id = value;
+                break;
+            case -1:
+                fprintf(stderr, "unknown subopt %s\n", saved);
+                exit(2);
+        }
+    }
+
+    wasi_ephemeral_nn_graph_execution_context c =
+        map_get(&contexts, context_id);
+    wasi_ephemeral_nn_error nnret;
+    nnret = wasi_ephemeral_nn_compute(c);
+    if (nnret != wasi_ephemeral_nn_error_success) {
+        fprintf(stderr, "compute failed with %d\n", (int)nnret);
+        exit(1);
+    }
+}
+
+static void
+get_output(char *options)
+{
+    int ret;
+    const char *outfile = NULL;
+    const char *context_id = "default";
+    uint32_t idx = 0;
+    enum {
+        opt_context_id,
+        opt_idx,
+        opt_file,
+    };
+    static char *const keylistp[] = {
+        [opt_context_id] = "context-id",
+        [opt_idx] = "idx",
+        [opt_file] = "file",
+        NULL,
+    };
+    while (*options) {
+        extern char *suboptarg;
+        char *value;
+        const char *saved = options;
+        switch (getsubopt(&options, keylistp, &value)) {
+            case opt_context_id:
+                if (value == NULL) {
+                    fprintf(stderr, "no value for %s\n", saved);
+                    exit(2);
+                }
+                context_id = value;
+                break;
+            case opt_file:
+                if (value == NULL) {
+                    fprintf(stderr, "no value for %s\n", saved);
+                    exit(2);
+                }
+                outfile = value;
+                break;
+            case opt_idx:
+                if (value == NULL) {
+                    fprintf(stderr, "no value for %s\n", saved);
+                    exit(2);
+                }
+                idx = atoi(value);
+                break;
+            case -1:
+                fprintf(stderr, "unknown subopt %s\n", saved);
+                exit(2);
+        }
+    }
+
+    int outfd = -1;
+    if (outfile != NULL) {
+        outfd = open(outfile, O_CREAT | O_TRUNC | O_WRONLY);
+        if (outfd == -1) {
+            fprintf(stderr, "failed to open output file \"%s\": %s\n", outfile,
+                    strerror(errno));
+            exit(1);
+        }
+    }
+
+    wasi_ephemeral_nn_error nnret;
+    wasi_ephemeral_nn_graph_execution_context c =
+        map_get(&contexts, context_id);
+    void *resultbuf = NULL;
+    size_t resultbufsz = 256;
+    uint32_t resultsz;
+retry:
+    resultbuf = realloc(resultbuf, resultbufsz);
+    if (resultbuf == NULL) {
+        exit(1);
+    }
+    nnret =
+        wasi_ephemeral_nn_get_output(c, 0, resultbuf, resultbufsz, &resultsz);
+    if (nnret == wasi_ephemeral_nn_error_too_large) {
+        resultbufsz *= 2;
+        goto retry;
+    }
+    if (nnret != wasi_ephemeral_nn_error_success) {
+        fprintf(stderr, "get_output failed with %d\n", (int)nnret);
+        exit(1);
+    }
+    if (outfd != -1) {
+        ssize_t written = write(outfd, resultbuf, resultsz);
+        if (written == -1) {
+            fprintf(stderr, "failed to write: %s\n", strerror(errno));
+            exit(1);
+        }
+        if (written == -1) {
+            fprintf(stderr, "unexpetecd write length %zu (expected %zu)\n",
+                    written, (size_t)resultsz);
+            exit(1);
+        }
+        ret = close(outfd);
+        if (ret != 0) {
+            fprintf(stderr, "failed to close: %s\n", strerror(errno));
+            exit(1);
+        }
+    }
+    else {
+        fprintf(stderr, "WARNING: discarding %zu bytes output\n",
+                (size_t)resultsz);
+    }
+}
+
+enum longopt {
+    opt_load_graph = 0x100,
+    opt_init_execution_context,
+    opt_set_input,
+    opt_compute,
+    opt_get_output,
+};
+
+static const struct option longopts[] = {
+    {
+        "load-graph",
+        required_argument,
+        NULL,
+        opt_load_graph,
+    },
+    {
+        "init-execution-context",
+        optional_argument,
+        NULL,
+        opt_init_execution_context,
+    },
+    {
+        "set-input",
+        required_argument,
+        NULL,
+        opt_set_input,
+    },
+    {
+        "compute",
+        optional_argument,
+        NULL,
+        opt_compute,
+    },
+    {
+        "get-output",
+        optional_argument,
+        NULL,
+        opt_get_output,
+    },
+    {
+        NULL,
+        0,
+        NULL,
+        0,
+    },
+};
+
+int
+main(int argc, char **argv)
+{
+    extern char *optarg;
+    int ch;
+    int longidx;
+    while ((ch = getopt_long(argc, argv, "", longopts, &longidx)) != -1) {
+        switch (ch) {
+            case opt_load_graph:
+                load_graph(optarg);
+                break;
+            case opt_init_execution_context:
+                init_execution_context(optarg ? optarg : "");
+                break;
+            case opt_set_input:
+                set_input(optarg);
+                break;
+            case opt_compute:
+                compute(optarg ? optarg : "");
+                break;
+            case opt_get_output:
+                get_output(optarg ? optarg : "");
+                break;
+            default:
+                exit(2);
+        }
+    }
+    exit(0);
+}

+ 58 - 0
wamr-wasi-extensions/samples/nn-cli/map.c

@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2025 Midokura Japan KK.  All rights reserved.
+ * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "map.h"
+
+static uintmax_t *
+map_find_slot(struct map *m, const char *name)
+{
+    size_t i;
+    for (i = 0; i < m->nentries; i++) {
+        if (!strcmp(m->entries[i].k, name)) {
+            return &m->entries[i].v;
+        }
+    }
+    return NULL;
+}
+
+static void
+map_append(struct map *m, const char *k, uintmax_t v)
+{
+    m->entries = realloc(m->entries, (m->nentries + 1) * sizeof(*m->entries));
+    if (m->entries == NULL) {
+        exit(1);
+    }
+    struct map_entry *e = &m->entries[m->nentries++];
+    e->k = k;
+    e->v = v;
+}
+
+void
+map_set(struct map *m, const char *k, uintmax_t v)
+{
+    uintmax_t *p = map_find_slot(m, k);
+    if (p != NULL) {
+        fprintf(stderr, "duplicated id \"%s\"\n", k);
+        exit(1);
+    }
+    map_append(m, k, v);
+}
+
+uintmax_t
+map_get(struct map *m, const char *k)
+{
+    uintmax_t *p = map_find_slot(m, k);
+    if (p == NULL) {
+        fprintf(stderr, "id \"%s\" not found\n", k);
+        exit(1);
+    }
+    return *p;
+}

+ 19 - 0
wamr-wasi-extensions/samples/nn-cli/map.h

@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2025 Midokura Japan KK.  All rights reserved.
+ * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+ */
+
+#include <stdint.h>
+
+struct map {
+    struct map_entry {
+        const char *k;
+        uintmax_t v;
+    } * entries;
+    size_t nentries;
+};
+
+void
+map_set(struct map *m, const char *k, uintmax_t v);
+uintmax_t
+map_get(struct map *m, const char *k);

+ 6 - 0
wamr-wasi-extensions/test.sh

@@ -20,6 +20,12 @@ cmake -B build-app-nn \
 samples/nn
 cmake --build build-app-nn
 
+cmake -B build-app-nn-cli \
+-DCMAKE_TOOLCHAIN_FILE=${WASI_SDK}/share/cmake/wasi-sdk.cmake \
+-DCMAKE_PREFIX_PATH=${PREFIX} \
+samples/nn-cli
+cmake --build build-app-nn-cli
+
 cmake -B build-app-socket-nslookup \
 -DCMAKE_TOOLCHAIN_FILE=${WASI_SDK}/share/cmake/wasi-sdk-pthread.cmake \
 -DCMAKE_PREFIX_PATH=${PREFIX} \