WebAssembly / wasi-sdk

WASI-enabled WebAssembly C/C++ toolchain
Apache License 2.0
1.27k stars 190 forks source link

Using C++ string results in `fd_close`, `fd_seek` & `fd_write`, even for BAREMETAL compile #220

Open ArjaanBuijk opened 2 years ago

ArjaanBuijk commented 2 years ago

I am writing C++ code for the internet computer, and I am a bit stuck at the moment trying to use strings.

I studied the discussion in this issue in great detail, and I made good progress after I edited the WASI SDK’s Makefile to disable assertions while opting into baremetal mode:

LIBCXXABI_CMAKE_FLAGS = \
    # ... snip ...
    -DUNIX:BOOL=ON \
+   -DLIBCXXABI_BAREMETAL=ON \
+   -DLIBCXXABI_ENABLE_ASSERTIONS=OFF \
    --debug-trycompile

With this patch, I could get the following to work:

// file: ok.cpp
#include <stdint.h>
#include <string.h>

// See: https://lld.llvm.org/WebAssembly.html#imports
#define WASM_SYMBOL_IMPORTED(module, name) \
    __attribute__((import_module(module))) __attribute__((import_name(name)));

// See: https://lld.llvm.org/WebAssembly.html#exports
#define WASM_SYMBOL_EXPORTED(name) asm(name) __attribute__((visibility("default")));

namespace ic0
{
    void debug_print(uint32_t src, uint32_t size)
        WASM_SYMBOL_IMPORTED("ic0", "debug_print");
}

void debug_print(const char *message)
{
    ic0::debug_print((uint32_t)message, (uint32_t)strlen(message));
}

void api_empty_to_empty() WASM_SYMBOL_EXPORTED("canister_query api_empty_to_empty");
void api_empty_to_empty()
{

    // Verify that debug logs are working (only on local network)
    const char *pmessage = "Using const char* - Hello from C++ api_empty_to_empty!";
    debug_print(pmessage);

    debug_print("Using directly - Hello from C++ api_empty_to_empty!");
}

I compile it with:

<...>/build/wasi-sdk-14.<xxx>/bin/clang++ --target=wasm32-wasi -O3 -flto -fcxx-exceptions --sysroot <...>/build/wasi-sdk-14.<xxx>/share/wasi-sysroot -std=c++20 -nostartfiles -Wl,--no-entry -Wl,--lto-O3 -Wl,--strip-all -Wl,--strip-debug -Wl,--stack-first -Wl,--export-dynamic ok.cpp -o ok.wasm

When converting the ok.wat using wasm2wat, the only import function is the one I specify, and the file ok.wat is really tiny:

(module
  (type (;0;) (func (param i32 i32)))
  (type (;1;) (func))
  (import "ic0" "debug_print" (func (;0;) (type 0)))
  (func (;1;) (type 1))
  (func (;2;) (type 1)
    call 1
    call 1)
  (func (;3;) (type 1)
    i32.const 65588
    i32.const 54
    call 0
    i32.const 65536
    i32.const 51
    call 0)
  (func (;4;) (type 1)
    call 3
    call 2)
  (table (;0;) 1 1 funcref)
  (memory (;0;) 2)
  (global (;0;) (mut i32) (i32.const 65536))
  (export "memory" (memory 0))
  (export "canister_query api_empty_to_empty" (func 4))
  (data (;0;) (i32.const 65536) "Using directly - Hello from C++ api_empty_to_empty!\00Using const char* - Hello from C++ api_empty_to_empty!\00"))

This file ok.wasm could be deployed without any issues.

Then I tried to use a string, and that did not work:

// file: nok.cpp
#include <stdint.h>
#include <string>

// See: https://lld.llvm.org/WebAssembly.html#imports
#define WASM_SYMBOL_IMPORTED(module, name) \
    __attribute__((import_module(module))) __attribute__((import_name(name)));

// See: https://lld.llvm.org/WebAssembly.html#exports
#define WASM_SYMBOL_EXPORTED(name) asm(name) __attribute__((visibility("default")));

namespace ic0
{
    void debug_print(uint32_t src, uint32_t size)
        WASM_SYMBOL_IMPORTED("ic0", "debug_print");
}

void debug_print(const std::string message)
{
    ic0::debug_print((uint32_t)message.c_str(), (uint32_t)message.size());
}

void api_empty_to_empty() WASM_SYMBOL_EXPORTED("canister_query api_empty_to_empty");
void api_empty_to_empty()
{
    /*
   * This does NOT work, it creates the imports:
   * (import "wasi_snapshot_preview1" "fd_close" (func (;1;) (type 1)))
   * (import "wasi_snapshot_preview1" "fd_seek" (func (;2;) (type 12)))
   * (import "wasi_snapshot_preview1" "fd_write" (func (;3;) (type 7)))
   */
    std::string message1 = "Create string first - Hello from C++ api_empty_to_empty!";
    debug_print(message1);
}

I compile it in the same way, but now the file nok.wat is much bigger and contains imports for fd_close, fd_seek & fd_write:

(module
  (type (;0;) (func (param i32 i32) (result i32)))
  ...
  (type (;42;) (func (param i32 i64 i64 i64 i64)))
  (import "ic0" "debug_print" (func (;0;) (type 10)))
  (import "wasi_snapshot_preview1" "fd_close" (func (;1;) (type 1)))
  (import "wasi_snapshot_preview1" "fd_seek" (func (;2;) (type 11)))
  (import "wasi_snapshot_preview1" "fd_write" (func (;3;) (type 7)))

I tried to apply the techniques explained in the other issue to hunt down the reason and then patch it, but was not able to.

Any tips how to get rid of these imports would be greatly appreciated.

sbc100 commented 2 years ago

I still think -Wl,--trace-symbol=__wasi_fd_close is going to be your best starting point. It should tell you which object file is causing that symbol to be include.

If you build with -g should also easily be able to see where in the resulting .wat file those imports are used/needed. You can also use wasm-objdump -d -x foo.wasm to inspect the resulting binary without using wat format.

sbc100 commented 2 years ago

Out of interest is there some fundamental reason why you don't use those wasi_snapshot_preview1 imports? It is just code size or are you trying to target an environment that doesn't support WASI? If its the latter, it seems rather odd to be using WASI SDK at all. Obviously we always want to generate the smallest possible binaries but targeting non-WASI host environments is probably out of scope of wasi-sdk.