Compiling Typescript to native code

Gaurav Gautam
4 min readOct 5, 2023

Static hermes (shermes), the next iteration of the hermes javascript engine is adding support for compiling typescript/flow to native code. While shermes is not released yet the code is available on the github repository. In this post I will compile a simple typescript program to C. Watch this for more context.

Compiling shermes

At the time of writing built binaries cannot be obtained for static hermes. To compile it from source on macos follow these steps.

Get the source from the github repository and switch to the static_h branch.

mkdir hermes_workspace
cd hermes_workspace
git clone git@github.com:facebook/hermes.git
cd hermes
git checkout static_h
cd ..

Make sure you have the build dependencies mentioned here: https://hermesengine.dev/docs/building-and-running/ . Then in the hermes_workspacefolder run these to build. Note that these commands build hermes in debug mode which is slower. Release mode build instructions are at the end of this post.

cmake -S hermes -B build -G Ninja
cmake --build ./build

Now you should have a shermesbinary in the hermes_workspace/build/bindirectory.

Compiling a typescript file to C

Now create a typescript file in the `hermes_workspace` directory. You can call it main.ts

function sum(args: number[]) {
let result: number = 0;
for (let index = 0; index < args.length; ++index) {
result += args[index];
}
}

const result = sum([1, 2, 3, 4, 5.1]);
print("=======");
print(result);

You can execute this file by running

./build/bin/shermes -typed -exec ./main.ts

You can ask shermesto output the compiled C for this typescript file like this

./build/bin/shermes -typed -emit-c ./main.ts

The C output generated by shermes is quite readable as I (not a C developer) was able to find the place where the array was getting created and the addition was being done.

The array getting created in the compiled C output

np1 = _sh_ljs_double(0);
np4 = _sh_ljs_double(1);
locals.t0 = _sh_new_fastarray(shr, 5);
_sh_fastarray_push(shr, &np4, &locals.t0);
np0 = _sh_ljs_double(2);
_sh_fastarray_push(shr, &np0, &locals.t0);
np0 = _sh_ljs_double(3);
_sh_fastarray_push(shr, &np0, &locals.t0);
np0 = _sh_ljs_double(4);
_sh_fastarray_push(shr, &np0, &locals.t0);
np0 = _sh_ljs_double(((struct HermesValueBase){.raw = 4617428107952285286u}).f64);
_sh_fastarray_push(shr, &np0, &locals.t0);

Addition of the values being done in C

L1:
;
// PhiInst
// PhiInst
np0 = _sh_fastarray_load(shr, &locals.t0, _sh_ljs_get_double(np2));
np3 = _sh_ljs_double(_sh_ljs_get_double(np3) + _sh_ljs_get_double(np0));
np2 = _sh_ljs_double(_sh_ljs_get_double(np2) + _sh_ljs_get_double(np4));
np0 = _sh_fastarray_length(shr, &locals.t0);
np0 = _sh_ljs_bool(_sh_ljs_get_double(np2) < _sh_ljs_get_double(np0));
np1 = np3;
if(_sh_ljs_get_bool(np0)) goto L1;
goto L2;
L2:
;
// PhiInst
goto L3;

You can also ask shermes to create a binary executable by running this

./build/bin/shermes -typed -o a.out ./main.ts

Calling a C function from typescript

You can compile your own C functions into a library and then link them to your typescript program using shermes. To do this first we will need to write some C function.

Create the following files

- hermesworkspace
- build
- hermes
+ mydir
+ include
myfunc.h
+ lib
myfunc.c

In myfunc.h add this

#ifndef MYFUNC_H
#define MYFUNC_H
int exportedmethod(int);
#endif

In myfunc.c add this

#include <myfunc.h>
int exportedmethod(int a) {
return a + 2;
}

Now set the following environment variables

export CPATH=<your_filesystem>/workingdir_hermes/mydir/include
export LIBRARY_PATH=<your_filesystem>/workingdir_hermes/mydir/lib

And compile the C program from the workingdir_hermes/mydir folder as follows

cc -c -o lib/myfunc.o myfunc.c

This will create a myfunc.o file in the workingdir_hermes/mydir/libfolder.
Now we are ready to write a typescript program that will call the function exportedmethod.To do this create a typescript file with the following contents. Note that I do not understand why I need the { throw 0; } at the end of the function declaration. I just tried it and it worked.

const _myfunc = $SHBuiltin.extern_c({ include: "myfunc.h" }, function exportedmethod(input: c_int): c_int {
throw 0;
});
const result = _myfunc(40);
print(result);

To run this you have to use the l flag to tell shermes to link against your library. Run it as follows.

./build/bin/shermes -typed -exec -lmyfunc.o ./yourfile.ts

This should print 42 as a result.

More examples

You can find more examples of usage in the hermes/examples/ffi folder. This folder includes the examples that were shown in the announcement video from earlier in this article.

As you may have already deduced, I have no connections to the hermes project. The announcement video piqued my curiosity about static hermes and I wanted to share the steps I followed to try it out.

Updates

  • I was made aware that the commands I have used above for building hermes build it in debug mode, which is slower. To build in release mode you can use
cmake -S hermes -B build_release -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build ./build_release
  • The -emit-c option is a temporary feature that is there for easing the development of hermes.
  • Typescript support is still experimental and incomplete, but being improved.

--

--