Skip to content

Added Emscripten examples #184

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .github/workflows/ros-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3

- name: Update ROS signing key (per ROS migration guide)
run: |
sudo rm /usr/share/keyrings/ros2-latest-archive-keyring.gpg
sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros2-latest-archive-keyring.gpg
# - name: Update ROS signing key (per ROS migration guide)
# run: |
# sudo rm /usr/share/keyrings/ros2-latest-archive-keyring.gpg
# sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros2-latest-archive-keyring.gpg

- name: Configure environment and install Conan release
run: |
Expand All @@ -44,10 +44,10 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3

- name: Update ROS signing key (per ROS migration guide)
run: |
sudo rm /usr/share/keyrings/ros2-latest-archive-keyring.gpg
sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros2-latest-archive-keyring.gpg
# - name: Update ROS signing key (per ROS migration guide)
# run: |
# sudo rm /usr/share/keyrings/ros2-latest-archive-keyring.gpg
# sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros2-latest-archive-keyring.gpg

- name: Configure environment and install Conan from develop2 branch
run: |
Expand Down
14 changes: 14 additions & 0 deletions examples/cross_build/emscripten/bindings/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
cmake_minimum_required(VERSION 3.15)
project(wasm_example CXX)

find_package(Eigen3 REQUIRED)
find_package(ZLIB REQUIRED)
find_package(fmt REQUIRED)
add_executable(wasm_example main.cpp)

target_link_libraries(${PROJECT_NAME} PRIVATE Eigen3::Eigen ZLIB::ZLIB fmt::fmt)

# Set the executable suffix to .html in order to generate a html page by
# Emscripten (there is no way of setting this from a user toolchain or
# conanfile as it is later overridden by the Emscripten toolchain)
set(CMAKE_EXECUTABLE_SUFFIX ".html")
39 changes: 39 additions & 0 deletions examples/cross_build/emscripten/bindings/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# WASM project with bindings and conan dependency

## Build and run

To compile the project:

```sh
$ conan build . -pr:h ../profiles/wasm32 --build=missing
```

To open a WASM webpage locally, most of the browsers will complain due to security reasons as WASM must be loaded asynchronous

The easiest way of opening the generated webpage (should be in `build/release-wasm/wasm_example.html`) is by running a local server.
This can be done via `emrun` command:

`emrun` is packaged with `emskd` recipe so it should be available by activating build environment:

**POSIX**
```sh
$ source build/release-wasm/generators/conanbuild.sh
```

**Windows**
```sh
$ build\release-wasm\generators\conanbuild.bat
```

By this time, `emrun`, `node`, and other JS/WASM tools should be available in the path:

```sh
$ emrun --browser <browser_name> build/release-wasm/wasm_example.html
```

Or using python `http.server` module:

```sh
$ python -m http.server 8080
```
Then, navigating to your build folder and open `wasm_example.html`
8 changes: 8 additions & 0 deletions examples/cross_build/emscripten/bindings/ci_test_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import os
from test.examples_tools import run

run("conan build . --build=missing --profile:host ../profiles/wasm32")

assert os.path.exists(os.path.join("build", "release-wasm", "wasm_example.html"))
assert os.path.exists(os.path.join("build", "release-wasm", "wasm_example.js"))
assert os.path.exists(os.path.join("build", "release-wasm", "wasm_example.wasm"))
43 changes: 43 additions & 0 deletions examples/cross_build/emscripten/bindings/conanfile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from conan import ConanFile
from conan.tools.cmake import CMake, CMakeDeps, CMakeToolchain, cmake_layout
from conan.errors import ConanInvalidConfiguration


class WasmExampleRecipe(ConanFile):
name = "wasm-example"
version = "1.0"
package_type = "application"
settings = "os", "compiler", "build_type", "arch"

def layout(self):
cmake_layout(self)

def requirements(self):
self.requires("eigen/3.4.0")
self.requires("zlib/1.3.1")
self.requires("fmt/11.1.4")

def validate(self):
if self.settings.os != "Emscripten":
raise ConanInvalidConfiguration("This example is only supported on Emscripten.")

def generate(self):
deps = CMakeDeps(self)
deps.generate()
tc = CMakeToolchain(self)

# HEAPxx values need to be exported explicitly since Emscripten 4.0.7
# https://github.com/emscripten-core/emscripten/blob/main/ChangeLog.md#407---041525
tc.extra_exelinkflags.append(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not best practice, as it is hardcoded in recipe for all profiles.
Either this recipe is exclusively for webassembly, or maybe define it elsewhere? I understand that the main.cpp is only for emscripten right now, so maybe a validate() that raises if not emcc?

"-sEXPORTED_FUNCTIONS=['_malloc','_free'] \
-sEXPORTED_RUNTIME_METHODS=['ccall','cwrap','getValue','setValue','HEAPF32'] \
-sALLOW_MEMORY_GROWTH=1 \
-sNO_EXIT_RUNTIME=1 \
--shell-file ${CMAKE_SOURCE_DIR}/shell.html"
)
tc.generate()

def build(self):
cmake = CMake(self)
cmake.configure()
cmake.build()
59 changes: 59 additions & 0 deletions examples/cross_build/emscripten/bindings/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#include <Eigen/Core>
#include <cstdint>
#include <emscripten/emscripten.h>
#include <fmt/printf.h>
#include <iostream>
#include <string>
#include <zlib.h>

#ifdef __cplusplus
#define EXTERN extern "C"
#else
#define EXTERN
#endif

EXTERN EMSCRIPTEN_KEEPALIVE uint32_t fib(uint32_t n) {
std::cout << "Calculating Fibonacci for n = " << n << std::endl;
if (n <= 1)
return n;
uint32_t a = 0, b = 1, c;
for (uint32_t i = 2; i <= n; ++i) {
c = a + b;
a = b;
b = c;
}
return c;
}

EXTERN EMSCRIPTEN_KEEPALIVE void printMessage(const char *message) {
std::cout << "Message from C: " << message << std::endl;
std::string script =
"alert('Message from C++: " + std::string(message) + "')";
std::cout << "Executing script: " << script << std::endl;
emscripten_run_script(script.c_str());
}

EXTERN EMSCRIPTEN_KEEPALIVE void addOne(int32_t *input, int32_t *output) {
*output = *input + 1;
}

EXTERN EMSCRIPTEN_KEEPALIVE float sumArray(const float *data, int32_t size) {
fmt::print("Data input: ");
for (int i = 0; i < size; ++i) {
fmt::print("{} ", data[i]);
}
std::cout << std::endl;
Eigen::Map<const Eigen::ArrayXf> vec(data, size);
return vec.sum();
}

EXTERN EMSCRIPTEN_KEEPALIVE void getZlibVersion() {
fmt::print("Zlib version being used: {}\n", zlibVersion());
}

int main() {
std::cout << "Hello World!" << std::endl;
auto data = new float[5]{1.0f, 2.0f, 3.0f, 4.0f, 5.0f};
std::cout << sumArray(data, 5) << std::endl;
fmt::print(zlibVersion());
}
166 changes: 166 additions & 0 deletions examples/cross_build/emscripten/bindings/shell.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<!doctype html>
<html lang="en-us">
<body>
<h1>Conan C++ Emscripten Example</h1>
<br />
<div id="status">Downloading...</div>
<div>
<progress value="0" max="100" id="progress" hidden="1"></progress>
</div>
<textarea id="output" rows="8" style="width: 100%"></textarea>
<hr />

<!-- Example of calling JS from C -->
<button onclick="printMessage()">Print Message</button>
<hr />

<button onclick="getZlibVersion()">Print Zlib version</button>
<hr />

<!-- Example of a simple invocation to fibonachi -->
<input type="number" id="fibInput" placeholder="e.g., 10" />
<button onclick="fibExample()">Compute Fibonacci</button>
<p id="fibResult"></p>
<hr />

<!-- Example of a function call using two buffers -->
<button onclick="pressBtn()">Click me to increase counter!</button>
<p id="counterResult"></p>
<hr />

<!-- Example of a function call using a Float32Array and Eigen -->
<input
type="text"
id="numbersInput"
placeholder="e.g., 42.2, 2.1, 8"
size="50"
/>
<button onclick="sumExample()">Compute Float32 Sum with Eigen</button>
<p id="sumResult"></p>

<script type="text/javascript">
var statusElement = document.getElementById("status");
var progressElement = document.getElementById("progress");

var Module = {
print: (function () {
var element = document.getElementById("output");
if (element) element.value = ""; // clear browser cache
return (...args) => {
var text = args.join(" ");
console.log(text);
if (element) {
element.value += text + "\n";
element.scrollTop = element.scrollHeight; // focus on bottom
}
};
})(),
setStatus: (text) => {
Module.setStatus.last ??= { time: Date.now(), text: "" };
if (text === Module.setStatus.last.text) return;
var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
var now = Date.now();
if (m && now - Module.setStatus.last.time < 30) return; // if this is a progress update, skip it if too soon
Module.setStatus.last.time = now;
Module.setStatus.last.text = text;
if (m) {
text = m[1];
progressElement.value = parseInt(m[2]) * 100;
progressElement.max = parseInt(m[4]) * 100;
progressElement.hidden = false;
} else {
progressElement.value = null;
progressElement.max = null;
progressElement.hidden = true;
}
statusElement.innerHTML = text;
},
totalDependencies: 0,
monitorRunDependencies: (left) => {
this.totalDependencies = Math.max(this.totalDependencies, left);
Module.setStatus(
left
? "Preparing... (" +
(this.totalDependencies - left) +
"/" +
this.totalDependencies +
")"
: "All downloads complete.",
);
},
};
Module.setStatus("Downloading...");
window.onerror = () => {
Module.setStatus("Exception thrown, see JavaScript console");
Module.setStatus = (text) => {
if (text) console.error("[post-exception status] " + text);
};
};

// Example of auto string handle by WASM on simple string parameter passing
const printMessage = () => {
Module.ccall(
"printMessage",
null,
["string"],
["Hello from C++ WebAssembly!"],
);
};
const getZlibVersion = () => {
Module.ccall("getZlibVersion", null, [], []);
};

// Example of a simple invocation to fibonachi
const fibExample = () => {
const fib = Module.cwrap("fib", "number", ["number"]); // returns a number
const input = parseInt(document.getElementById("fibInput").value);
if (isNaN(input)) {
alert("Please enter a valid integer.");
return;
}
const result = fib(input);
document.getElementById("fibResult").textContent =
"Fibonacci of " + input + " is: " + result;
};

var value = 0; // (static) value to increment by one
const pressBtn = () => {
const addOne = Module.cwrap("addOne", null, ["number", "number"]); // void function
// alloc 4 bytes of memory for the input and 4 for the output (32-bit integers)
const inputPtr = Module._malloc(4);
const outputPtr = Module._malloc(4);

Module.setValue(inputPtr, value, "i32");
addOne(inputPtr, outputPtr);
const result = Module.getValue(outputPtr, "i32");
value = result;
document.getElementById("counterResult").textContent = "Sum: " + result;

// dealloc memory to avoid memory leaks
Module._free(inputPtr);
Module._free(outputPtr);
};

const sumExample = () => {
const sumArray = Module.cwrap("sumArray", "number", [
"number",
"number",
]);
// Get the input string and split by commas
const inputStr = document.getElementById("numbersInput").value;
const numberStrings = inputStr.split(",").map((s) => s.trim());

// Convert to Float32Array
const inputArray = new Float32Array(numberStrings.map(Number));
const len = inputArray.length;
const bytesPerElement = inputArray.BYTES_PER_ELEMENT;
const inputPtr = Module._malloc(len * bytesPerElement);
Module.HEAPF32.set(inputArray, inputPtr / bytesPerElement);
const result = sumArray(inputPtr, len);
Module._free(inputPtr);
document.getElementById("sumResult").textContent = "Sum: " + result;
};
</script>
{{{ SCRIPT }}}
</body>
</html>
8 changes: 8 additions & 0 deletions examples/cross_build/emscripten/profiles/asmjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
include(./emsdk-base)

[settings]
arch=asm.js

[conf]
tools.build:exelinkflags+=['-sMAXIMUM_MEMORY=2GB', '-sINITIAL_MEMORY=64MB']
tools.build:sharedlinkflags+=['-sMAXIMUM_MEMORY=2GB', '-sINITIAL_MEMORY=64MB']
Loading