Skip to content
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

How to use TypeScript declarations for Emscripten #10271

Open
kbumsik opened this issue Jan 23, 2020 · 23 comments
Open

How to use TypeScript declarations for Emscripten #10271

kbumsik opened this issue Jan 23, 2020 · 23 comments

Comments

@kbumsik
Copy link
Contributor

kbumsik commented Jan 23, 2020

Related: #9674 #7083

I would like to discuss the current best way to use typing features of TypeScript with Emscripten. If you are looking for a WebIDL -> TypeScript .d.ts converter for C++ application specifically, you may refer to the above two issues.

I recently found @types/emscripten from NPM and I spent a few hours to figure out how to integrate it with my Emscripten project and contributed the package a little bit too. The .d.ts typing file can be found here: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/emscripten/index.d.ts

Suppose I make add.c like this:

// add.c
int add (int a, int b) {
	return a + b;
}

Two ways I have figured out:

  1. add--post-js.ts
  2. add.d.ts + add--post-js.js

In my case I found option 2 is better. Will be explained later.


1. add--post-js.ts

add—post-js.ts

/// <reference types="emscripten" />
/** Above will import declarations from @types/emscripten, including Module etc. */

// This will merge to the existing EmscriptenModule interface from @types/emscripten
// If this doesn't work, try globalThis.EmscriptenModule instead.
interface EmscriptenModule {
	// Module.cwrap() will be available by doing this.
	// Requires -s "EXTRA_EXPORTED_RUNTIME_METHODS=['cwrap']"
	cwrap: typeof cwrap;
	// Exported from add.cpp
	// Requires "EXPORTED_FUNCTIONS=['_add']"
	_add(number, number): number;
	// or using cwrap. See below
	add(number, number): number;
}

Module['onRuntimeInitialized'] = function() {
  // Just Module._add() will work, but I'm just demonstrating usage of cwrap()
  Module['add'] = cwrap('add', 'number', ['number', 'number']);
}

2. add.d.ts + add--post-js.js

add.d.ts

/// <reference types="emscripten" />
/** Above will import declarations from @types/emscripten, including Module etc. */

// This will merge to the existing EmscriptenModule interface from @types/emscripten
// If this doesn't work, try globalThis.EmscriptenModule instead.
export interface AddModule extends EmscriptenModule {
	// Module.cwrap() will be available by doing this.
	// Requires -s "EXTRA_EXPORTED_RUNTIME_METHODS=['cwrap']"
	cwrap: typeof cwrap;
	// Exported from add.cpp
	// Requires "EXPORTED_FUNCTIONS=['_add']"
	_add(number, number): number;
	// or using cwrap. See below
	add(number, number): number;
}

// Declare any name
declare const addModule: AddModule;
// Only for -s MODULARIZE=1
export = addModule;
// Only for -s MODULARIZE=1 -s EXPORT_ES6=1
export default addModule;

add—post-js.js

/// <reference types="emscripten" />
/** Above will import declarations from @types/emscripten, including Module etc. */
/** It is not .ts file but declaring reference will pass TypeScript Check. */

Module['onRuntimeInitialized'] = function() {
  // Just Module._add() will work, but I'm just demontrating usage of cwrap
  Module['add'] = cwrap('add', 'number', ['number', 'number']);
}

I know, both of them don't particularly look pretty but they are the cleanest ones so far.

I found that the second one is better for two reasons: add--post-js.ts file must be compiled to js in order to work with emcc command. And there are problems when you want to use export statement because -s MODULARIZE=1 creates another export statement automatically.

Please share any better tricks if you have one. Also you will notice that @types/emscripten is not complete when you look at the source code it would be great if anyone can make the package better.

Thanks :)

@kbumsik kbumsik changed the title Typescript typings for Emscripten TypeScript declarations for Emscripten Jan 24, 2020
@kbumsik kbumsik changed the title TypeScript declarations for Emscripten How to use TypeScript declarations for Emscripten Jan 24, 2020
@kripken
Copy link
Member

kripken commented Jan 24, 2020

cc @jgravelle-google (maybe this could benefit from interface types?)

@jgravelle-google
Copy link
Contributor

If I understand correctly, this is about using C++ functions in a TypeScript application? And ideally having TS types for the C++ as well.

Probably the most ergonomic tool would be using embind's EMSCRIPTEN_BINDINGS mechanism (https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html#a-quick-example). Interface types should help here in the future, by giving us a custom section a TS tool could autogenerate the .d.ts from.

@kbumsik
Copy link
Contributor Author

kbumsik commented Jan 27, 2020

@jgravelle-google Right. This is the current available way for someone who want to use C/C++ functions in a TS application right now. I am not asking Emscripten devs to do something. I posted this because I cannot find any guides on how to achieve this on the internet, and I thought posting it as an issue here is better than writing it in my blog because someone might have a better idea.

As you mention, In the future we could automatically generate .d.ts using embind or the WebIDL binder. I believe #9674 and #7083 already discuss about it.

@jgravelle-google
Copy link
Contributor

I am not asking Emscripten devs to do something.

Oh, I assumed you weren't yeah. Just thinking out loud "if I were to do this what would I do," in case that counts as having a better idea. And also thinking about how Interface Types would apply here specifically.

WebIDL binder is probably closer to what TS would expect. Is there already a general-purpose .d.ts generator from webidl for web APIs? Though I imagine there's still some bridging needed to make the formats line up.

@kbumsik
Copy link
Contributor Author

kbumsik commented Jan 27, 2020

Is there already a general-purpose .d.ts generator from webidl for web APIs?

I think microsoft/TSJS-lib-generator is the best bet. The TypeScript Team do not hand-write Web API type definitions from scratch but generate .d.ts files from various WebIDL sources (Source: microsoft/TypeScript#3027). I haven't done any experiments with this but I guess we need some work to adapt it to Emscripten.

@unicomp21
Copy link

unicomp21 commented May 24, 2020

@jgravelle-google With protobuj.js supporting typescript, and the fact we're moving to a place where we'll want to interop across multiple languages (typescript, mono/c#, golang, rust, kotlin, c++, etc.) within the browser/webassembly, would it make more sense to focus on good protobuf support for emscripten? Then we can start using "event bus" patterns within the browser/webassembly to interop across different webassembly languages.

@giniedp
Copy link

giniedp commented Jul 12, 2020

I have written a tool that can generate type definitions for emscripten modules.

For now i have tested it only with the ammo.js project

The tool itself is based on the https://github.com/microsoft/TSJS-lib-generator. Unfortunately TSJS-lib-generator itself can not be used for emscripten as it is very specific in some parts. I borrowed some code and concepts to make it work for ammo and hopefully for other emscripten projects as well. Just needs some more testing to discover edge cases.

@kripken
Copy link
Member

kripken commented Jul 13, 2020

Very interesting @giniedp !

If you think it's ready, a PR to add a link to that in the docs would be great.

@Michael-Pascale
Copy link

Hi!

Im super new to typescript, and fairly novice with javascript. I was looking at the code above, and I am a bit confused. I have a very simple c file, with an add method like above, and I have compiled it using this command:

emcc -s WASM=1 -s MODULARIZE=1 -o simple.js simple.c. 

Im not sure how I go from having this simple.js file to importing it into a typescript file and running it using ts-node. I am guessing I need to write the files above, but I dont know how they connect to the wasm/js file generated with emcc, or how to use them to actually run the web assembly.

My goal in this is to run some webassembly of some c code I am writing in an angular app. There are some guides, but I cant seem to figure them out if they arent outdated.

@unicomp21
Copy link

Doesn't the rust webassembly stuff have good support for typescript? I'm wondering if rust webassembly linked w/ emscripten webassembly, especially if the rust was generated, could be another path to getting generated typescript bindings?

@unicomp21
Copy link

unicomp21 commented Apr 16, 2021

There is also the whole thing of using protobufs to describe marshaled api's. Protobuf.js already has this covered for typescript. I "think" rust webassembly has good support for it too. Which brings me back to the idea of generating rust somehow, linking w/ emscripten, and then running the api via protobufjs/typescript.

Some are arguing about performance of protobufs, ie the overhead of zigzag integer encoding. When laying out a protobuf schema, there are scalars which don't get zigzag encoded at runtime. So, the protobuf schema author has a choice with respect to choosing performance over the serialized encoding size. In a nutshell, I'm not sure the argument has legs.

@julianrpereira
Copy link

Hi @Michael-Pascale I am in the same dilemma as you were. I have the emcc built js and wasm files and I need to import them into a typescript react app.
Would appreciate any help if you figured it out pls.

@imxood
Copy link

imxood commented Oct 18, 2021

I added a basic example, please refer to~
https://github.com/imxood/emcc-typescript-example
@julianrpereira

@PoignardAzur
Copy link

I really wish this was implemented.

"Being able to check that types are the same of both sides of the FFI barrier" feels like an extremely basic use-case, and yet it's doesn't seem to be covered anywhere in the wasm ecosystem.

That's fine if your wasm library is intended to be imported directly from the html file and take control of the entire app (which is basically the emscripten golden path, since it's the one you see in eg in-browser game engines), but if you want to export a wasm library as JS somehow, then the lack of type safety becomes painful fast.

Honestly, the fact that tickets like this have existed basically since WebAssembly came out with no progress in sight is a pretty bad omen for the wasm ecosystem. At the very least, it tells me that the ecosystem isn't interested in my use-cases (basically anything that isn't a video game or a tech demo).

@dhdaines
Copy link

dhdaines commented Jul 8, 2022

Thank you for this, it seems that people do not write documentation anymore, only bug reports. It took me a lot of swearing to understand what is actually going on here. The fact that TypeScript uses magical comments to import magical modules installed from NPM is not what I expected! The fact that it gives utterly incomprehensible error messages like "statements are not allowed in ambient contexts" when you misplace a semicolon doesn't help either.

I have a library (https://github.com/ReadAlongs/SoundSwallower) compiled with -sMODULARIZE=1 that wraps C code using JavaScript classes defined inside the Module object. This is hideous awfulness, but it doesn't seem that there is any better way to do it with the Emscripten runtime being what it is, and EXPORT_ES6 being, apparently, broken (is this still true?) if you want to target Node.js and the web from the same code. Someone will surely explain to me that I just do not understand... anything, probably. I freely admit that I am but a lowly Python programmer, unacquainted with the ways of the world.

There are lots of more obvious and elegant examples of WASM libraries but they seem to be written in Rust, and I am not ready to rewrite my C code in Rust, though that would surely be a good idea. Nor do I want to be forced to use C++, which instantly bloats your code the instant you touch its standard library (and, it seems, maybe even if you don't).

The add.d.ts example above did not work for me but I was able to get TypeScript to accept something like it with a few adjustments. Unfortunately tsc requires you to pass various cryptic flags or add magical keys to tsconfig.json in order to avoid even more cryptic error messages or output code that just plain doesn't work (see https://stackoverflow.com/questions/56238356/understanding-esmoduleinterop-in-tsconfig-file for instance), but it seems that my test code succeeds with tsc --esModuleInterop --lib es2015,dom test.ts. I don't understand why this takes 5 seconds to compile and adds 3000 bytes to the output JavaScript code, but, hey, Types are Good, and at least it's not Java. Do not skip any of those flags or you will surely be confused.

Here is my .d.ts file, in case anybody has a similar use case. Note that you have to declare the constructor for your class as a new() method in a nested interface, which then returns a declared class (or an interface, but then you can't make it iterable, for instance)... the TypeScript documentation is completely useless on this point and StackOverflow isn't much better.

/// <reference types="emscripten" />
/* the comment above is magic */
declare class Config {
    /* do not declare the constructor, but declare everything else here */
    get(key: string): string|number;
    /* ... other methods */
}
interface SoundSwallowerModule extends EmscriptenModule {
    get_model_path(string): string; /* a function */
    Config: { /* a class, but just its constructor */
        new(dict?: Object): Config;
    }
    /* ... other functions and classes */
}
declare const Module: EmscriptenModuleFactory<SoundSwallowerModule>;
export default Module;

And my test code:

import soundswallower_factory from "soundswallower";
import {SoundSwallowerModule, Config} from "soundswallower";
(async () => {
    const soundswallower: SoundSwallowerModule = await soundswallower_factory();
    const config: Config = new soundswallower.Config();
    console.log(config.get("samprate"));
})();

@luxaritas
Copy link
Contributor

Am I correct in understanding that the state of the art here is the wit format and wit-bindgen? Could emscripten itself generate a wit?

@luxaritas
Copy link
Contributor

luxaritas commented Oct 30, 2022

Or actually, if I'm understanding correctly, this is predicated on supporting interface types period, which emscripten doesn't do, and I don't know what the state of the spec actually is at this point

@pnocera
Copy link

pnocera commented Dec 7, 2023

@dhdaines great comment. And thanks for your blog series about TypeScript modules With Emscripten and CMake, which were an unvalueable help to me along my quest.
I'm providing the link for others : TypeScript modules With Emscripten and CMake

@dhdaines
Copy link

dhdaines commented Dec 7, 2023

@dhdaines great comment. And thanks for your blog series about TypeScript modules With Emscripten and CMake, which were an unvalueable help to me along my quest. I'm providing the link for others : TypeScript modules With Emscripten and CMake

Thanks! They might be out of date, as the Emscripten maintainers have fixed many of the issues that I ran into (and to be fair many of them are not Emscripten's fault...)

@dsogari
Copy link

dsogari commented Jan 28, 2024

@luxaritas

Am I correct in understanding that the state of the art here is the wit format and wit-bindgen?

I believe so.

Could emscripten itself generate a wit?

In fact, I'm fidgeting with the idea of developing a Clang plugin for generating interface files from C++ headers. The choice of files would be:

  • .idl (Web IDL)
  • .wit (Wasm Interface Type)
  • .d.ts (TypeScript declaration)

Of course, only the C++ features that have a direct mapping to the concepts in each of these formats would be supported.

Any thoughts?

@sbc100
Copy link
Collaborator

sbc100 commented Jan 28, 2024

@brendandahl

@brendandahl
Copy link
Collaborator

It looks like it hasn't been mentioned yet, but embind now supports TypeScript generation from bindings.

@andumen
Copy link

andumen commented Feb 6, 2024

It looks like it hasn't been mentioned yet, but embind now supports TypeScript generation from bindings.

the .d.ts is the interface, but where is the extend or implement ? the cpp implement function but typescript not ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests