-
Notifications
You must be signed in to change notification settings - Fork 63
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
base: main
Are you sure you want to change the base?
Changes from 11 commits
d451607
a69d48c
b516bd2
cdfc45d
e8c9be6
709e2f9
899bb1d
98d7b4f
23e9dc6
239b6dd
fce069e
edeef51
599e8d0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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") |
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` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import os | ||
from test.examples_tools import run | ||
|
||
run('conan config install https://github.com/perseoGI/conan-toolchains.git -sf conan_config --args "-b pgi/new/emsdk"') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is exactly being config-installed from your fork of the conan-toolchains? Does that repo contain configuration too? Sounds not possible for the examples repo, this should be more straightforward. |
||
run("conan build . --build=missing --profile:host emsdk/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")) |
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( | ||
perseoGI marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"-sEXPORTED_FUNCTIONS=['_malloc','_free'] \ | ||
-sEXPORTED_RUNTIME_METHODS=['ccall','cwrap','getValue','setValue','HEAPF32'] \ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() |
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()); | ||
} |
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> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
cmake_minimum_required(VERSION 3.15) | ||
project(wasm-alloc CXX) | ||
|
||
add_executable(wasm-alloc main.cpp) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this was already fixed in main branch, could you sync your branch?