Skip to content
Open
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"))
38 changes: 38 additions & 0 deletions examples/cross_build/emscripten/bindings/conanfile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from conan import ConanFile
from conan.tools.cmake import CMake, CMakeDeps, CMakeToolchain, cmake_layout


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 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(
"-sEXPORTED_FUNCTIONS=['_malloc','_free'] \
-sEXPORTED_RUNTIME_METHODS=['ccall','cwrap','getValue','setValue','HEAPF32'] \
Copy link
Member

Choose a reason for hiding this comment

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

I am still a bit surprised of seeing these flags in recipes by default. Shouldn't flags belong to user toolchains somehow and not Conan? Where does users that use Emscripten put these flags if not using Conan?

-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(base)

[settings]
arch=asm.js

[conf]
tools.build:exelinkflags+=['-sMAXIMUM_MEMORY=2GB', '-sINITIAL_MEMORY=64MB']
tools.build:sharedlinkflags+=['-sMAXIMUM_MEMORY=2GB', '-sINITIAL_MEMORY=64MB']
39 changes: 39 additions & 0 deletions examples/cross_build/emscripten/profiles/base
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Note: this profile uses emsdk package from Conan Center Index
Copy link
Member

Choose a reason for hiding this comment

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

Call this profile emcc_base or something like that?

# To use a local emsdk installation could be done by:
# a) Define a platform_tool_requires ensuring CC, CXX, etc are defined correctly in PATH
# [platform_tool_requires]
# emsdk/<version>
# b) Define compiler_executables
# tools.build:compiler_executables={'c':'emcc', 'cpp':'em++'}
# c) Define buildenv
# [buildenv]
# CC=<path/to/emcc>
# CXX=<path/to/em++>
# AR ..

[settings]
build_type=Release
compiler=emcc
compiler.cppstd=17
compiler.libcxx=libc++
compiler.version=4.0.9
os=Emscripten

[tool_requires]
emsdk/4.0.9
ninja/[*]

[conf]
tools.build:exelinkflags=['-sALLOW_MEMORY_GROWTH=1']
tools.build:sharedlinkflags=['-sALLOW_MEMORY_GROWTH=1']

# Set Ninja as default generator as it is faster and will sove issues on Windows
tools.cmake.cmaketoolchain:generator=Ninja
Copy link
Member

Choose a reason for hiding this comment

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

What is the default generator for emcc? Unix/MinGW Makefiles?


# Verbosity to see emcc invocations
tools.build:verbosity=verbose
tools.compilation:verbosity=verbose

# Distinguish between architectures
tools.cmake.cmake_layout:build_folder_vars=['settings.build_type', 'settings.arch']

8 changes: 8 additions & 0 deletions examples/cross_build/emscripten/profiles/wasm32
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
include(base)

[settings]
arch=wasm

[conf]
tools.build:exelinkflags+=['-sMAXIMUM_MEMORY=4GB', '-sINITIAL_MEMORY=64MB']
tools.build:sharedlinkflags+=['-sMAXIMUM_MEMORY=4GB', '-sINITIAL_MEMORY=64MB']
10 changes: 10 additions & 0 deletions examples/cross_build/emscripten/profiles/wasm64
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
include(base)

[settings]
arch=wasm64

[conf]
# In this early stage of wasm64, ALLOW_MEMORY_GROWTH is not having effect. Also it may not be the most efficient solution.
# wasm64 for now needs to declare INITIAL_MEMORY as the maximum memory
tools.build:exelinkflags+=['-sMAXIMUM_MEMORY=16GB', '-sINITIAL_MEMORY=16GB']
tools.build:sharedlinkflags+=['-sMAXIMUM_MEMORY=16GB', '-sINITIAL_MEMORY=16GB']
Loading
Loading