Static Hermes is the the next major version of Hermes, still under active development. It enables optional ahead-of-time native compilation of soundly typed JavaScript, using TypeScript or Flow type annotations.
As part of RN’s cross- platform vision, we want to empower developers like Joe to not just create harmless JavaScript crashes, but REAL fast native crashes too!
Introducing Static Hermes ● Optional ahead of time compilation to native code ● C performance with JS usability ● Mix and match bytecode and native code based on your needs ● Requires sound types* for performance
Status/Disclaimer ● Static Hermes is not ready for general usage yet, but we have positive indications of performance ● This is a work in progress, and we are sharing it early to show you what we’ve been working on and gather community feedback ● We want to thank Amazon for adding TypeScript support to Static Hermes.
Why Static Hermes? ● Hermes improves startup through bytecode, but there are limits on interpreter performance after startup ● Writing C++ to improve performance is difficult ● RN developers are already accustomed to writing typed JS code ● We want to take advantage of this to bridge predictable C performance with JS usability via native AOT compilation.
Why is it difficult to compile JS ahead-of-time? function addXY(x, y) { return x + y; } • We don’t know the type of x and y ahead of time. • All possible types (string, array, number, etc) and combinations need to be supported. • The result is either a size explosion or interpreter-like performance. • Solutions in academia, specifically HopC, by Manuel Serrano, do not meet our goal (not ruling it out in the future) And why is no one doing it?
Can’t type annotations help? function addXY(x: number, y: number) { return x + y; } • No! Because type annotations in TypeScript and Flow are unsound. • The type annotations do not guarantee that types are correct at runtime. • The compiler cannot rely on type annotations for better code.
Example of type unsoundness function addXY(x: number, y: number) { return x + y; } let a: number[] = [10, 20, 30]; // Uh-oh! Adding undefined + undefined! addXY(a[10], a[11]); Despite the type annotations, both parameters can be non- numbers at runtime.
Sound Types ● Static Hermes modifies some JS semantics to allow efficient sound typing ● The new semantics to improve performance are opt-in on a granular level ● New code can coexist and interop with unmodified code ● Both TypeScript and Flow are supported
Are we breaking JavaScript? ● We are enforcing the semantics the user wanted ● When they wrote x: number, they didn’t intend that x could also be undefined. ● Technically that was a bug which cannot be caught at compile time ● We are strengthening the existing TypeScript and Flow type systems by enforcing them at runtime. ● It should not make a difference for correct code. ● It is all opt-in. The standard behavior is available, if you want it.
Interpreter vs typed native nbody.js Nbody is a well-known benchmark used in The Computer Language Benchmarks Game, simulating movement of celestial bodies. This is a math heavy benchmark with many property accesses. SH improves runtime from 5511 ms to 565 ms
Understanding the problem 01 Writing native extensions is complex and error prone. It requires switching between languages. 02 Calling existing native APIs requires writing JSI wrappers in C++. 03 Crossing JSI is not practical when we need high frequency interop with JS. The cost of JSI itself starts to dominate performance.
Zero-cost FFI ● No conversions, indirections, allocations ● Just a C function call ● Call overhead between 15x to 80x lower than JSI. Not percent, times!
Zero-cost FFI ● Native functions can be directly invoked from “unsafe” sections of JS code ● Explicit distinction between “safe” and “unsafe” code ● Native functions and native types are 1st class citizens of the language, enabling no-overhead compilation
Reading SQLite DB in a few lines of JS, no C++ required // Open the database res = _sqlite3_open( stringToAsciiz(”file.db"), dbPtrBuffer ); if (res !== 0) throw Error(res); db = _sh_ptr_read_ptr(dbPtrBuffer, 0); // Prepare the SQL statement sqlPtr = stringToAsciiz( "SELECT id, name FROM my_table” ); res = _sqlite3_prepare_v2( db, sqlPtr, -1, dbPtrBuffer, c_null ); if (res !== 0) throw Error(res); stmt = _sh_ptr_read_ptr(dbPtrBuffer, 0); // Loop through all rows in the table let row = 1; while (_sqlite3_step(stmt) === SQLITE_ROW) { const id = _sqlite3_column_int(stmt, 0); const namePtr = _sqlite3_column_text(stmt, 1); const name = asciizToString(namePtr, 1024); print(`row${row++}:`, id, name); }
Example: getenv const _getenv = $SHBuiltin.extern_c({}, function getenv(name: c_ptr): c_ptr {throw 0;}); function getenv(name: string): string { "use unsafe"; let name_z = stringToAsciiz(name); try { let val_z = _getenv(name_z); return asciizToString_unsafe(val_z, 2048); } finally { free(name_z); } } print(getenv("PATH")); Convert the name from a JS string to a native C string
Anatomy of an Extern const _getenv = $SHBuiltin.extern_c({}, function getenv(name: c_ptr): c_ptr { throw 0; }); Throw away body to satisfy the type checker.
Thank you! ● For questions or suggestions, please use the Discussions tab on the Hermes GitHub repo. https://github.com/facebook/hermes/discussions/categories/static-hermes ● nbody.js: https://github.com/facebook/hermes/blob/static_h/benchmarks/nbody/fully-typed/nbody.js ● FFI: https://github.com/facebook/hermes/tree/static_h/examples/ffi