Calling JavaScript code from C++ via emscripten::val offers no type-safety.
typescripten uses TypeScript interface definition files to generate type-safe C++ interfaces for JavaScript and TypeScript libraries. Compare the JavaScript statements in the comments to the C++ code:
int main() {
// var elem = document.createElement("p")
auto elem = js::document()->createElement(js::string("p"));
// elem.innerText = "Hello CppCon 2021"
elem->innerText(js::string("Hello CppCon 2021"));
// elem.style.fontSize = 20.0
elem->style()->fontSize(js::string("20vh"));
// document.body.appendChild(elem)
js::document()->body()->appendChild(elem);
}
typescripten uses the parser API provided by typescript. It is now self-hosting, i.e., it can the parse interface definition file for the typescript parser itself.
typescripten has been presented at CppCon 2021 and in a slightly extended talk at CppNow 2022.
Let's say you have a typescript module MyLib.d.ts
for a JavaScript library you have written yourself, or maybe for a JavaScript API to some web service:
declare namespace MyLib {
function appendNumber(a: string, b: number): string;
}
Running the typescripten compiler on this file will produce the C++ header MyLib.d.h
namespace tc::js_defs {
using namespace jst; // no ADL
struct _impl_js_jMyLib;
using _js_jMyLib = ref<_impl_js_jMyLib>;
struct _impl_js_jMyLib : virtual object_base {
struct _tcjs_definitions {
static auto appendNumber(string a, double b) noexcept;
};
};
inline auto _impl_js_jMyLib::_tcjs_definitions::appendNumber(string a, double b) noexcept {
return emscripten::val::global("MyLib")["appendNumber"](a, b).template as<string>();
}
}; // namespace tc::js_defs
namespace tc::js {
using MyLib = js_defs::_js_jMyLib;
} // namespace tc::js
By including the generated header you can use MyLib
from C++ in a type-safe way.
#include "MyLib.d.h"
#include <iostream>
int main() {
std::cout << tc::explicit_cast<std::string>( // Unpack the JavaScript string
tc::js::MyLib::appendNumber(
tc::js::string("foobar"), // Create a JavaScript string
20 // JavaScript number type maps to double
)
) << std::endl;
}
See typescripten/tests
and typescriptenc/tests
for more examples.
Close analogues are Rust's stdweb and wasm-bindgen.
typescripten/
contains the code to support basic Typescript types and language features
any
,undefined
,null
andstring
types- Support for mixed enums like
enum E {
a,
b = "I'm a string",
c = 5.0
}
- Passing C++ functions and lambdas as callbacks to JavaScript
- union types
A|B|C
expressed as template classunion_t<A, B, C>
- JavaScript library types like
Array
,ReadonlyArray
,Promise
andIterable
. These should be themselves generated in the future.
typescripten supports
- type guards
- type aliases
- optional arguments and optional member properties
- generics, see #3
Some TypeScript constructs are not yet supported. Better support for generic type constraints and indexed access types are high priority and coming soon.
Both the TypeScript Playground and the TypeScript AST Viewer (Check the JS debug console!) are invaluable when you want to improve typescripten.
- Python 3
- Emscripten 2.0.18 or newer (Node.js and NPM are included)
- think-cell public library
- boost 1.73.0
It is easiest to download typescripten
from npm.
npm install -g @think-cell/typescripten
I am working on integrating typescripten with emscripten.
If you want to include it in a project of yours, add the following to your Cmake file:
# Download typescripten (The name must not be changed)
include(FetchContent)
FetchContent_Declare(
typescripten
GIT_REPOSITORY https://github.com/think-cell/typescripten
)
FetchContent_MakeAvailable(typescripten)
# Include the CMake helper function
include(${typescripten_SOURCE_DIR}/cmake/TypeScripten.cmake)
# Install the types you need via npm (at configure time)
execute_process(COMMAND npm install WORKING_DIRECTORY ${PROJECT_SOURCE_DIR})
# Add your executable target
add_executable(test ...)
# Compile the downloaded TypeScript interface definitions (here lib.dom.d.ts)
# into a header before 'test' is built.
add_typescripten_target(
TARGET test
INPUTS ${PROJECT_SOURCE_DIR}/node_modules/typescript/lib/lib.dom.d.ts
OUTPUT lib.dom.d.h
)
Now you can include <lib.dom.d.h>
in your C++ files. In order to build your project, download emscripten and boost, and run
source path/to/emsdk/emsdk_env.sh
emcmake cmake -S . -B build
cmake --build build
See example
and its associated README.
typescriptenc
debug builds are built with DWARF debug information. Google Chrome Dev builds can interactively debug WebAssembly applications with debug info. See this Chrome blog entry on how to setup Chrome for debugging.
In order to build with debugging support set the DEBUG_DEVTOOLS
options. If you build the typescriptenc_debug
target, cmake will package typescripten
with webpack so it can run in a browser and it will start a web server for you:
source path/to/emsdk/emsdk_env.sh
emcmake cmake -S . -B build -DDEBUG_DEVTOOLS=ON
cmake --build build --target typescriptenc_debug
Now you should be able to open [http://localhost:8000/debug] in Chrome and in the DevTools console you should be able to see the C++ sources, set breakpoints, look at local variables etc. This debug build cannot read local files from disk, so the input file is hard-coded in debug/index.html
. Change that file and rebuild or change the live version in build/debug/index.html
and reload the running page.
We use Hungarian Notation in our code base, i.e.,
- Global variables start with
g_
- Member fields start with
m_
- Constants start with
c_
All variables are prefixed with a type tag <type-tag>Name
Name
is camel-caseint
's type tag isn
, e.g.nArguments
std::string
's type tag isstr
std::vector<T>
's type tag isvec<type-tag-of-T>
, name is singular, e.g.vecstrArguments
jst::ref<T>
's type tag isj<type-tag-of-T>
ts::Symbol
's type tag issym
.jst::optional<T>
's type tag iso<type-tag-of-T>
.
Example: jst::ref<jst::optional<ts::Symbol>> josymDeclaration;
There should be no duplicates between type tag and name, e.g. vecstrArguments
, not vecstrArgumentsVector