diff --git a/.github/actions/wasi/base/action.yml b/.github/actions/wasi/base/action.yml new file mode 100644 index 0000000..71b4f8a --- /dev/null +++ b/.github/actions/wasi/base/action.yml @@ -0,0 +1,16 @@ +name: "Ubuntu base dependencies" + +runs: + using: "composite" + steps: + - name: Install system dependencies + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y ninja-build + + - name: Install wasmtime CLI + shell: bash + run: | + curl https://wasmtime.dev/install.sh -sSf | bash + sudo cp ~/.wasmtime/bin/wasmtime /usr/bin/wasmtime diff --git a/.github/actions/wasi/clang/action.yml b/.github/actions/wasi/clang/action.yml new file mode 100644 index 0000000..7284c61 --- /dev/null +++ b/.github/actions/wasi/clang/action.yml @@ -0,0 +1,15 @@ +name: "Ubuntu latest clang" + +runs: + using: "composite" + steps: + - name: Install clang/llvm 17 + shell: bash + run: | + sudo apt-add-repository 'deb https://apt.llvm.org/jammy llvm-toolchain-jammy-17 main' + sudo wget -qO /etc/apt/trusted.gpg.d/llvm.asc https://apt.llvm.org/llvm-snapshot.gpg.key + sudo apt-get update + sudo apt-get install -y -t llvm-toolchain-jammy-17 \ + clang-17 llvm-17 lld-17 lldb-17 libc++-17-dev \ + libc++abi-17-dev libclang-rt-17-dev + for f in /usr/lib/llvm-*/bin/*; do sudo ln -sf "$f" /usr/bin; done diff --git a/.github/ci.yaml b/.github/ci.yaml index 1aa56c1..a0a377b 100644 --- a/.github/ci.yaml +++ b/.github/ci.yaml @@ -16,6 +16,7 @@ jobs: { os: macos-latest, preset: x64-darwin-clang-dynamic }, { os: windows-latest, preset: x64-windows-msvc-dynamic }, { os: windows-latest, preset: x64-windows-clang-dynamic }, + { os: ubuntu-latest, preset: wasm32-wasi-clang-static }, ] steps: @@ -31,3 +32,11 @@ jobs: shell: bash run: | cmake --workflow --preset ${{matrix.target.preset}} + + - name: Upload vcpkg error logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: logs-${{matrix.target.preset}} + path: | + /usr/local/share/vcpkg/**/*.log diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d73fdbd..2b9e054 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,11 +12,11 @@ jobs: { os: ubuntu-latest, preset: x64-linux-gcc-dynamic }, { os: ubuntu-latest, preset: x86-linux-gcc-dynamic }, { os: ubuntu-latest, preset: x64-linux-clang-dynamic }, - { os: ubuntu-latest, preset: x86-linux-clang-dynamic }, { os: macos-latest, preset: x64-darwin-gcc-dynamic }, { os: macos-latest, preset: x64-darwin-clang-dynamic }, { os: windows-latest, preset: x64-windows-msvc-dynamic }, { os: windows-latest, preset: x64-windows-clang-dynamic }, + { os: ubuntu-latest, preset: wasm32-wasi-clang-static }, ] steps: @@ -37,3 +37,11 @@ jobs: shell: bash run: | cmake --workflow --preset ${{matrix.target.preset}} + + - name: Upload vcpkg error logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: logs-${{matrix.target.preset}} + path: | + /usr/local/share/vcpkg/**/*.log diff --git a/CMakePresets.json b/CMakePresets.json index 6d4744f..e26094d 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -8,6 +8,7 @@ "include": [ "cmake/presets/arm64-darwin-gcc.json", "cmake/presets/arm64-darwin-clang.json", + "cmake/presets/wasm32-wasi-clang.json", "cmake/presets/x64-darwin-gcc.json", "cmake/presets/x64-darwin-clang.json", "cmake/presets/x64-linux-gcc.json", diff --git a/README.md b/README.md index 4b83610..8f8662b 100644 --- a/README.md +++ b/README.md @@ -62,4 +62,18 @@ find_package( CONFIG REQUIRED) target_link_libraries( PUBLIC ::) ``` -This will require that your library is published and installed via vcpkg or found locally by setting the `CMAKE_PREFIX_PATH` environment variable in your other project during configure. \ No newline at end of file +This will require that your library is published and installed via vcpkg or found locally by setting the `CMAKE_PREFIX_PATH` environment variable in your other project during configure. + +If you wish to expose parts of your library as a WebAssembly module, you can add the `extern "C" __attribute__((export_name("")))` annotation to any function you wish to expose in `src/wasm/interface.cpp`, and compile using the `wasm32-wasi-clang` preset. This will generate a minimal WebAssembly binary exposing your exported functions at `build//src/wasm//lib.wasm`. This binary conforms to the [WASI WebAssembly standard](https://wasi.dev/), so it can be utilized in any WASI-supporting runtime like [`wasmer.io`](https://wasmer.io/) or [wasmtime](https://wasmtime.dev/). + +A WebAssembly version of your CLI will also be available: + +```sh +wasmtime run build//src/cli//_cli + +# Prints: +# version: 0.0.1 +``` + +> [!NOTE] +> Exposed WebAssembly functions currently only accept parameters and return values of numerical type. This is how core WebAssembly files work, and without additional glue-code or binding generators like [wit-bindgen](https://github.com/bytecodealliance/wit-bindgen), complex data-types cannot be sent over the runtime's ABI boundary. Complex data must be transferred via pointer and serialized bytes in exported/shared memory. \ No newline at end of file diff --git a/cmake/presets/toolchains/wasi-sdk.json b/cmake/presets/toolchains/wasi-sdk.json new file mode 100644 index 0000000..0605cc0 --- /dev/null +++ b/cmake/presets/toolchains/wasi-sdk.json @@ -0,0 +1,13 @@ +{ + "version": 6, + "configurePresets": [ + { + "name": "wasi-sdk", + "hidden": true, + "cacheVariables": { + "VCPKG_CHAINLOAD_TOOLCHAIN_FILE": "${sourceDir}/cmake/toolchains/wasi-sdk.toolchain.cmake", + "CMAKE_CROSSCOMPILING_EMULATOR": "wasmtime" + } + } + ] +} \ No newline at end of file diff --git a/cmake/presets/wasm32-wasi-clang.json b/cmake/presets/wasm32-wasi-clang.json new file mode 100644 index 0000000..a382b01 --- /dev/null +++ b/cmake/presets/wasm32-wasi-clang.json @@ -0,0 +1,52 @@ +{ + "version": 6, + "include": [ + "base.json", + "compilers/clang.json", + "toolchains/wasi-sdk.json" + ], + "configurePresets": [ + { + "name": "wasm32-wasi-clang-static", + "inherits": [ + "base", + "wasi-sdk", + "clang" + ], + "displayName": "WASM32 WASI clang static libs" + } + ], + "buildPresets": [ + { + "name": "wasm32-wasi-clang-static", + "inherits": "base", + "configurePreset": "wasm32-wasi-clang-static" + } + ], + "testPresets": [ + { + "name": "wasm32-wasi-clang-static", + "inherits": "base", + "configurePreset": "wasm32-wasi-clang-static" + } + ], + "workflowPresets": [ + { + "name": "wasm32-wasi-clang-static", + "steps": [ + { + "type": "configure", + "name": "wasm32-wasi-clang-static" + }, + { + "type": "build", + "name": "wasm32-wasi-clang-static" + }, + { + "type": "test", + "name": "wasm32-wasi-clang-static" + } + ] + } + ] +} \ No newline at end of file diff --git a/cmake/toolchains/wasi-sdk.toolchain.cmake b/cmake/toolchains/wasi-sdk.toolchain.cmake new file mode 100644 index 0000000..f3f78d3 --- /dev/null +++ b/cmake/toolchains/wasi-sdk.toolchain.cmake @@ -0,0 +1,14 @@ +include(FetchContent) + +set(FETCHCONTENT_FULLY_DISCONNECTED_OLD ${FETCHCONTENT_FULLY_DISCONNECTED}) +set(FETCHCONTENT_FULLY_DISCONNECTED OFF) +FetchContent_Declare( + wasi_sdk_toolchain + SOURCE_DIR "${CMAKE_BINARY_DIR}/_deps/wasi-sdk" + GIT_REPOSITORY https://github.com/rioam2/wasi-sdk-toolchain.git + GIT_TAG wasi-sdk-23 +) +FetchContent_MakeAvailable(wasi_sdk_toolchain) +set(FETCHCONTENT_FULLY_DISCONNECTED ${FETCHCONTENT_FULLY_DISCONNECTED_OLD}) + +include("${wasi_sdk_toolchain_SOURCE_DIR}/wasi-sdk.toolchain.cmake") \ No newline at end of file diff --git a/cmake/vcpkg/triplets/wasm32-wasi.cmake b/cmake/vcpkg/triplets/wasm32-wasi.cmake new file mode 100644 index 0000000..75fe210 --- /dev/null +++ b/cmake/vcpkg/triplets/wasm32-wasi.cmake @@ -0,0 +1,9 @@ +set(VCPKG_TARGET_ARCHITECTURE wasm32) +set(VCPKG_LIBRARY_LINKAGE static) +set(VCPKG_CRT_LINKAGE static) + +set(VCPKG_CMAKE_SYSTEM_NAME WASI) +set(VCPKG_CMAKE_SYSTEM_VERSION 1) +set(VCPKG_CMAKE_SYSTEM_PROCESSOR wasm32) + +set(VCPKG_CHAINLOAD_TOOLCHAIN_FILE "${CMAKE_CURRENT_LIST_DIR}/../../toolchains/wasi-sdk.toolchain.cmake") \ No newline at end of file diff --git a/cmake/vcpkg/vcpkg.toolchain.cmake b/cmake/vcpkg/vcpkg.toolchain.cmake index 99ca960..2d0f5a5 100644 --- a/cmake/vcpkg/vcpkg.toolchain.cmake +++ b/cmake/vcpkg/vcpkg.toolchain.cmake @@ -17,8 +17,8 @@ include(${CMAKE_CURRENT_LIST_DIR}/bootstrap/vcpkg-config.cmake) vcpkg_configure( CACHE_DIR_NAME @cpp_pt_name@ - REPO https://github.com/microsoft/vcpkg.git - REF 9edb1b8e590cc086563301d735cae4b6e732d2d2 # release 2023.08.09 + REPO https://github.com/rioam2/vcpkg-wasm32-wasi + REF 983e077b814c4c0e56336e1d32407c2b2c13a90b ) include($CACHE{_VCPKG_TOOLCHAIN_FILE}) diff --git a/init.cmake b/init.cmake index 2d62393..c89710e 100644 --- a/init.cmake +++ b/init.cmake @@ -65,6 +65,8 @@ set(templates src/${cpp_pt_module}/src/header-1.cpp src/cli/CMakeLists.txt src/cli/src/main.cpp + src/wasm/CMakeLists.txt + src/wasm/src/interface.cpp src/cmake/add_cpp_pt_executable.cmake src/cmake/add_cpp_pt_module.cmake src/cmake/cpp-pt-config.cmake diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 03034b3..baabe45 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -17,3 +17,7 @@ include(add_@cpp_pt_cmake@_executable) add_subdirectory(@cpp_pt_module@) add_subdirectory(cli) + +if (WASI) + add_subdirectory(wasm) +endif() diff --git a/src/wasm/CMakeLists.txt b/src/wasm/CMakeLists.txt new file mode 100644 index 0000000..07eefcd --- /dev/null +++ b/src/wasm/CMakeLists.txt @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: Copyright 2023 Mikhail Svetkin +# SPDX-License-Identifier: MIT + +add_@cpp_pt_cmake@_executable(wasm) + +target_sources(${@cpp_pt_cmake@_executable_target} PRIVATE + src/interface.cpp +) + +target_link_libraries(${@cpp_pt_cmake@_executable_target} + PRIVATE + @cpp_pt_name@::@cpp_pt_module@ +) + +set_target_properties(${@cpp_pt_cmake@_executable_target} PROPERTIES OUTPUT_NAME "lib@cpp_pt_module@.wasm") +target_link_options(${@cpp_pt_cmake@_executable_target} PRIVATE -nostartfiles -Wl,--no-entry) \ No newline at end of file diff --git a/src/wasm/src/interface.cpp b/src/wasm/src/interface.cpp new file mode 100644 index 0000000..76bfd98 --- /dev/null +++ b/src/wasm/src/interface.cpp @@ -0,0 +1,35 @@ +#include "@cpp_pt_name@/@cpp_pt_module@/@cpp_pt_module_header@.hpp" + +#include + +#include +#include +#include + +// Memory management exports +extern "C" __attribute__((export_name("wasm_malloc"))) void* wasm_malloc(size_t nbytes) { + return std::malloc(nbytes); +} + +extern "C" __attribute__((export_name("wasm_free"))) void wasm_free( + void* ptr) { + return std::free(ptr); +} + +// Exported WASM functions +extern "C" __attribute__((export_name("version"))) uint8_t* version() { + std::string version = @cpp_pt_name@::@cpp_pt_module@::version(); + uint8_t* versionPointer = (uint8_t*)wasm_malloc(version.length()); + std::memcpy(versionPointer, version.c_str(), version.length()); + return versionPointer; +} + +// This is the WASM module's initialization entrypoint. It should be called +// before running any other exported function because it initializes implicit +// global/static resources. This is only required in library/reactor mode of +// WebAssembly. Read more here: +// https://wasmcloud.com/blog/webassembly-patterns-command-reactor-library#the-reactor-pattern +extern "C" void __wasm_call_ctors(); +extern "C" __attribute__((export_name("_start"))) void _start() { + __wasm_call_ctors(); +} diff --git a/tests/cmake/add_cpp_pt_test.cmake b/tests/cmake/add_cpp_pt_test.cmake index e82ca2f..85fbffa 100644 --- a/tests/cmake/add_cpp_pt_test.cmake +++ b/tests/cmake/add_cpp_pt_test.cmake @@ -25,5 +25,9 @@ function(add_@cpp_pt_cmake@_test test_name) ${test_target} PRIVATE @cpp_pt_name@::${module_name} Catch2::Catch2WithMain ) + if (WASI) + target_link_options(${test_target} PRIVATE -nostartfiles -Wl,--no-entry) + endif() + catch_discover_tests(${test_target} TEST_PREFIX "${test_target}:" ${ARGN}) endfunction()