Skip to content

Commit

Permalink
feat(web): POC of Core WASM integration into Keyman Web
Browse files Browse the repository at this point in the history
- add temporary function to Core for this POC
- add new CoreProcessor to access Keyman Core WASM
- add unit tests for new core processor
- add code to KeymanEngine and InputProcessor to load the new CoreProcessor
- add web server to manual tests and new action `start` to build script

This change requires the manual tests to be loaded from a web server
instead of loaded as file, because otherwise the wasm code won't be
loaded.

Currently we always load CoreProcessor. This should be improved in a future
change to only load when it is actually needed.

Part-of: #11293
  • Loading branch information
ermshiperete committed Aug 20, 2024
1 parent 153683c commit 0abc250
Show file tree
Hide file tree
Showing 22 changed files with 339 additions and 10 deletions.
38 changes: 38 additions & 0 deletions core/src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ api_files = files(
'km_core_state_api.cpp',
'km_core_debug_api.cpp',
'km_core_processevent_api.cpp',
'wasm.cpp',
)

core_files = files(
Expand All @@ -133,6 +134,23 @@ mock_files = files(
'mock/mock_processor.cpp',
)

if cpp_compiler.get_id() == 'emscripten'
host_links = ['--whole-archive', '-sALLOW_MEMORY_GROWTH=1', '-sMODULARIZE=1', '-sEXPORT_ES6', '-sENVIRONMENT=webview', '--embind-emit-tsd', 'core-interface.d.ts', '-sERROR_ON_UNDEFINED_SYMBOLS=0']

if cpp_compiler.version().version_compare('>=3.1.44')
# emscripten 3.1.44 removes .asm object and so we need to export `wasmExports`
# #9375; https://github.com/emscripten-core/emscripten/blob/main/ChangeLog.md#3144---072523
links += ['-sEXPORTED_RUNTIME_METHODS=[\'UTF8ToString\',\'stringToNewUTF8\',\'wasmExports\']']
else
# emscripten < 3.1.44 does not include `wasmExports`
links += ['-sEXPORTED_RUNTIME_METHODS=[\'UTF8ToString\',\'stringToNewUTF8\']']
endif

links += [
# Forcing inclusion of debug symbols
'-g', '-Wlimited-postlink-optimizations', '--bind']
endif

lib = library('keymancore',
api_files,
core_files,
Expand Down Expand Up @@ -160,3 +178,23 @@ pkg.generate(
description: 'Keyman processor for KMN keyboards.',
subdirs: headerdirs,
libraries: lib)

if cpp_compiler.get_id() == 'emscripten'
# Build an executable
host = executable('core',
cpp_args: defns,
include_directories: inc,
link_args: links + host_links,
objects: lib.extract_all_objects(recursive: false))

if get_option('buildtype') == 'release'
# Split debug symbols into separate wasm file for release builds only
# as the release symbols will be uploaded to sentry
# custom_target('kmcmplib.wasm',
# depends: host,
# input: host,
# output: 'kmcmplib.wasm',
# command: ['wasm-split', '@OUTDIR@/wasm-host.wasm', '-o', '@OUTPUT@', '--strip', '--debug-out=@OUTDIR@/kmcmplib.debug.wasm'],
# build_by_default: true)
endif
endif
92 changes: 92 additions & 0 deletions core/src/wasm.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#ifdef __EMSCRIPTEN__
#ifdef __EMSCRIPTEN__
#include <emscripten/emscripten.h>
#include <emscripten/bind.h>

#else
#define EMSCRIPTEN_KEEPALIVE
#endif

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

#include <keyman_core.h>

constexpr km_core_attr const engine_attrs = {
256,
KM_CORE_LIB_CURRENT,
KM_CORE_LIB_AGE,
KM_CORE_LIB_REVISION,
KM_CORE_TECH_KMX,
"SIL International"
};

EMSCRIPTEN_KEEPALIVE km_core_attr const & tmp_wasm_attributes() {
return engine_attrs;
}

EMSCRIPTEN_BINDINGS(compiler_interface) {

// emscripten::class_<WasmCallbackInterface>("WasmCallbackInterface")
// .function("message", &WasmCallbackInterface::message, emscripten::pure_virtual())
// .function("loadFile", &WasmCallbackInterface::loadFile, emscripten::pure_virtual())
// .allow_subclass<WasmCallbackInterfaceWrapper>("WasmCallbackInterfaceWrapper")
// ;

// emscripten::class_<KMCMP_COMPILER_OPTIONS>("CompilerOptions")
// .constructor<>()
// .property("saveDebug", &KMCMP_COMPILER_OPTIONS::saveDebug)
// .property("compilerWarningsAsErrors", &KMCMP_COMPILER_OPTIONS::compilerWarningsAsErrors)
// .property("warnDeprecatedCode", &KMCMP_COMPILER_OPTIONS::warnDeprecatedCode)
// .property("shouldAddCompilerVersion", &KMCMP_COMPILER_OPTIONS::shouldAddCompilerVersion)
// .property("target", &KMCMP_COMPILER_OPTIONS::target)
// ;

// emscripten::class_<WASM_COMPILER_RESULT>("CompilerResult")
// .constructor<>()
// .property("result", &WASM_COMPILER_RESULT::result)
// .property("kmx", &WASM_COMPILER_RESULT::kmx)
// .property("kmxSize", &WASM_COMPILER_RESULT::kmxSize)
// .property("extra", &WASM_COMPILER_RESULT::extra)
// ;

// emscripten::class_<KMCMP_COMPILER_RESULT_MESSAGE>("CompilerResultMessage")
// .constructor<>()
// .property("errorCode", &KMCMP_COMPILER_RESULT_MESSAGE::errorCode)
// .property("lineNumber", &KMCMP_COMPILER_RESULT_MESSAGE::lineNumber)
// .property("columnNumber", &KMCMP_COMPILER_RESULT_MESSAGE::columnNumber)
// .property("filename", &KMCMP_COMPILER_RESULT_MESSAGE::filename)
// .property("parameters", &KMCMP_COMPILER_RESULT_MESSAGE::parameters)
// ;

// emscripten::class_<KMCMP_COMPILER_RESULT_EXTRA>("CompilerResultExtra")
// .constructor<>()
// .property("targets", &KMCMP_COMPILER_RESULT_EXTRA::targets)
// .property("kmnFilename", &KMCMP_COMPILER_RESULT_EXTRA::kmnFilename)
// .property("kvksFilename", &KMCMP_COMPILER_RESULT_EXTRA::kvksFilename)
// .property("displayMapFilename", &KMCMP_COMPILER_RESULT_EXTRA::displayMapFilename)
// .property("stores", &KMCMP_COMPILER_RESULT_EXTRA::stores)
// .property("groups", &KMCMP_COMPILER_RESULT_EXTRA::groups)
// ;

// emscripten::value_object<KMCMP_COMPILER_RESULT_EXTRA_STORE>("CompilerResultExtraStore")
// .field("storeType", &KMCMP_COMPILER_RESULT_EXTRA_STORE::storeType)
// .field("name", &KMCMP_COMPILER_RESULT_EXTRA_STORE::name)
// .field("line", &KMCMP_COMPILER_RESULT_EXTRA_STORE::line)
// ;

emscripten::value_object<km_core_attr>("km_core_attr")
.field("max_context", &km_core_attr::max_context)
.field("current", &km_core_attr::current)
.field("revision", &km_core_attr::revision)
.field("age", &km_core_attr::age)
.field("technology", &km_core_attr::technology)
//.field("vendor", &km_core_attr::vendor, emscripten::allow_raw_pointers())
;

emscripten::function("tmp_wasm_attributes", &tmp_wasm_attributes);
}
#endif
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion resources/build/minimum-versions.inc.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ KEYMAN_MIN_TARGET_VERSION_CHROME=95.0 # Final version that runs on Andro
# Dependency versions
KEYMAN_MIN_VERSION_NODE_MAJOR=20 # node version source of truth is /package.json:/engines/node
KEYMAN_MIN_VERSION_NPM=10.5.1 # 10.5.0 has bug, discussed in #10350
KEYMAN_MIN_VERSION_EMSCRIPTEN=3.1.44 # Warning: 3.1.45 is bad (#9529); newer versions work
KEYMAN_MIN_VERSION_EMSCRIPTEN=3.1.58
KEYMAN_MAX_VERSION_EMSCRIPTEN=3.1.58 # See #9529
KEYMAN_MIN_VERSION_VISUAL_STUDIO=2019
KEYMAN_MIN_VERSION_MESON=1.0.0
Expand Down
5 changes: 3 additions & 2 deletions web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ src/test/auto A Node-driven test suite for automated testing of Key

## Usage

Open **index.html** or **samples/index.html** in your browser. Be sure to
compile Keyman Engine for Web before viewing the pages.
Start the test server by running `./build.sh start`, then open
your browser to http://localhost:3000. Be sure to compile Keyman Engine
for Web before viewing the pages.

Refer to the samples for usage details.

Expand Down
8 changes: 8 additions & 0 deletions web/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ builder_describe "Builds engine modules for Keyman Engine for Web (KMW)." \
"clean" \
"configure" \
"build" \
"start Starts the test server" \
"test" \
"coverage Create an HTML page with code coverage" \
":app/browser The form of Keyman Engine for Web for use on websites" \
":app/webview A puppetable version of KMW designed for use in a host app's WebView" \
":app/ui Builds KMW's desktop form-factor keyboard-selection UI modules" \
":engine/attachment Subset used for detecting valid page contexts for use in text editing " \
":engine/core-processor Keyman Core WASM integration" \
":engine/device-detect Subset used for device-detection " \
":engine/dom-utils A common subset of function used for DOM calculations, layout, etc" \
":engine/events Specialized classes utilized to support KMW API events" \
Expand Down Expand Up @@ -56,6 +58,7 @@ builder_describe_outputs \
build:app/webview "/web/build/app/webview/${config}/keymanweb-webview.js" \
build:app/ui "/web/build/app/ui/${config}/kmwuitoggle.js" \
build:engine/attachment "/web/build/engine/attachment/lib/index.mjs" \
build:engine/core-processor "/web/build/engine/core-processor/lib/index.mjs" \
build:engine/device-detect "/web/build/engine/device-detect/lib/index.mjs" \
build:engine/dom-utils "/web/build/engine/dom-utils/obj/index.js" \
build:engine/events "/web/build/engine/events/lib/index.mjs" \
Expand Down Expand Up @@ -159,6 +162,8 @@ builder_run_child_actions build:engine/attachment
# Uses engine/interfaces (due to resource-path config interface)
builder_run_child_actions build:engine/keyboard-storage

builder_run_child_actions build:engine/core-processor

# Uses engine/interfaces, engine/device-detect, engine/keyboard-storage, & engine/osk
builder_run_child_actions build:engine/main

Expand Down Expand Up @@ -188,3 +193,6 @@ builder_run_action test test_action

# Create coverage report
builder_run_action coverage coverage_action

# Start the test server
builder_run_action start node src/tools/testing/test-server/index.cjs
14 changes: 10 additions & 4 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
"types": "./build/engine/attachment/obj/index.d.ts",
"import": "./build/engine/attachment/obj/index.js"
},
"./engine/interfaces": {
"es6-bundling": "./src/engine/interfaces/src/index.ts",
"types": "./build/engine/interfaces/obj/index.d.ts",
"import": "./build/engine/interfaces/obj/index.js"
"./engine/core-processor": {
"es6-bundling": "./src/engine/core-processor/src/index.ts",
"types": "./build/engine/core-processor/obj/index.d.ts",
"import": "./build/engine/core-processor/obj/index.js"
},
"./engine/device-detect": {
"es6-bundling": "./src/engine/device-detect/src/index.ts",
Expand All @@ -37,6 +37,11 @@
"types": "./build/engine/events/obj/index.d.ts",
"import": "./build/engine/events/obj/index.js"
},
"./engine/interfaces": {
"es6-bundling": "./src/engine/interfaces/src/index.ts",
"types": "./build/engine/interfaces/obj/index.d.ts",
"import": "./build/engine/interfaces/obj/index.js"
},
"./engine/js-processor": {
"es6-bundling": "./src/engine/js-processor/src/index.ts",
"types": "./build/engine/js-processor/obj/index.d.ts",
Expand Down Expand Up @@ -112,6 +117,7 @@
"@sentry/cli": "^2.31.0",
"@zip.js/zip.js": "^2.7.32",
"c8": "^7.12.0",
"express": "^4.19.2",
"jsdom": "^23.0.1",
"mocha": "^10.0.0"
},
Expand Down
4 changes: 4 additions & 0 deletions web/src/app/browser/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ compile_and_copy() {
mkdir -p "$KEYMAN_ROOT/web/build/app/resources/osk"
cp -R "$KEYMAN_ROOT/web/src/resources/osk/." "$KEYMAN_ROOT/web/build/app/resources/osk/"

# Copy the WASM host
cp "${KEYMAN_ROOT}/web/build/engine/core-processor/obj/import/core/"core.{js,wasm} "${KEYMAN_ROOT}/web/build/app/browser/debug/"
cp "${KEYMAN_ROOT}/web/build/engine/core-processor/obj/import/core/"core.{js,wasm} "${KEYMAN_ROOT}/web/build/app/browser/release/"

# Update the build/publish copy of our build artifacts
prepare

Expand Down
11 changes: 11 additions & 0 deletions web/src/app/webview/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,16 @@ compile_and_copy() {
mkdir -p "$KEYMAN_ROOT/web/build/app/resources/osk"
cp -R "$KEYMAN_ROOT/web/src/resources/osk/." "$KEYMAN_ROOT/web/build/app/resources/osk/"

# Copy the WASM host
cp "${KEYMAN_ROOT}/web/build/engine/core-processor/obj/import/core/"core.{js,wasm} "${KEYMAN_ROOT}/web/build/app/webview/debug/"
cp "${KEYMAN_ROOT}/web/build/engine/core-processor/obj/import/core/"core.{js,wasm} "${KEYMAN_ROOT}/web/build/app/webview/release/"

# Clean the sourcemaps of .. and . components
for script in "$KEYMAN_ROOT/web/build/$SUBPROJECT_NAME/debug/"*.js; do
if [[ "${script}" == *"/core.js" ]]; then
continue
fi

sourcemap="$script.map"
node "$KEYMAN_ROOT/web/build/tools/building/sourcemap-root/index.js" \
"$script" "$sourcemap" --clean --inline
Expand All @@ -68,6 +76,9 @@ compile_and_copy() {
# Do NOT inline sourcemaps for release builds - we don't want them to affect
# load time.
for script in "$KEYMAN_ROOT/web/build/$SUBPROJECT_NAME/release/"*.js; do
if [[ "${script}" == *"/core.js" ]]; then
continue
fi
sourcemap="$script.map"
node "$KEYMAN_ROOT/web/build/tools/building/sourcemap-root/index.js" \
"$script" "$sourcemap" --clean
Expand Down
1 change: 1 addition & 0 deletions web/src/engine/core-processor/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src/import/
64 changes: 64 additions & 0 deletions web/src/engine/core-processor/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/bin/env bash

## START STANDARD BUILD SCRIPT INCLUDE
# adjust relative paths as necessary
THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../../../resources/build/builder.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE

SUBPROJECT_NAME=engine/core-processor

. "${KEYMAN_ROOT}/web/common.inc.sh"
. "${KEYMAN_ROOT}/resources/shellHelperFunctions.sh"

# ################################ Main script ################################

builder_describe "Keyman Core WASM integration" \
"@/core:wasm" \
"clean" \
"configure" \
"build" \
"test" \
"--ci+ Set to utilize CI-based test configurations & reporting."

builder_describe_outputs \
configure "/web/src/engine/core-processor/src/import/core/core-interface.d.ts" \
build "/web/build/${SUBPROJECT_NAME}/lib/index.mjs"

builder_parse "$@"

#### Build action definitions ####

do_clean() {
rm -rf "${KEYMAN_ROOT}/web/build/${SUBPROJECT_NAME}"
rm -rf "src/import/"
}

do_configure() {
verify_npm_setup

mkdir -p "src/import/core/"
# we don't need this file here, but it's nice to have for reference and auto-completion
cp "${KEYMAN_ROOT}/core/build/wasm/${BUILDER_CONFIGURATION}/src/core-interface.d.ts" "src/import/core/"
}

copy_deps() {
mkdir -p "${KEYMAN_ROOT}/web/build/${SUBPROJECT_NAME}/obj/import/core/"
cp "${KEYMAN_ROOT}/core/build/wasm/${BUILDER_CONFIGURATION}/src/"core{.js,.wasm,-interface.d.ts} "${KEYMAN_ROOT}/web/build/${SUBPROJECT_NAME}/obj/import/core/"
}

do_build () {
copy_deps
compile "${SUBPROJECT_NAME}"

${BUNDLE_CMD} "${KEYMAN_ROOT}/web/build/${SUBPROJECT_NAME}/obj/index.js" \
--out "${KEYMAN_ROOT}/web/build/${SUBPROJECT_NAME}/lib/index.mjs" \
--format esm
}

builder_run_action clean do_clean
builder_run_action configure do_configure
builder_run_action build do_build

# No headless tests for this child project. Currently, DOM-based unit &
# integrated tests are run solely by the top-level $KEYMAN_ROOT/web project.
30 changes: 30 additions & 0 deletions web/src/engine/core-processor/src/core-processor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
type km_core_attr = import('./import/core/core-interface.js').km_core_attr;

export class CoreProcessor {
private instance: any;

/**
* Initialize Core Processor
* @param baseurl - The url where core.js is located
*/
public async init(baseurl: string): Promise<boolean> {

if (!this.instance) {
try {
const module = await import(baseurl + '/core.js');
this.instance = await module.default({
locateFile: function (path: string, scriptDirectory: string) {
return baseurl + '/' + path;
}
});
} catch (e: any) {
return false;
}
}
return !!this.instance;
};

public tmp_wasm_attributes(): km_core_attr {
return this.instance.tmp_wasm_attributes();
}
}
1 change: 1 addition & 0 deletions web/src/engine/core-processor/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './core-processor.js';
Loading

0 comments on commit 0abc250

Please sign in to comment.