As builders and developers, many of us are aware of the principle of Don’t Repeat Yourself (or DRY) and practice it every day. Entire runtimes and programming languages have been developed by taking that principle to an even higher level, with the core idea of writing software once and having it run on many different platforms, hardware, and operating systems. In this post, I explore the possibility of authoring and curating a software library in the TypeScript language, which at build time can then be generated into libraries in multiple other programming languages such as Python, Java, and .NET/C#. This is made possible by an open source software framework developed by AWS called jsii, one of the core architectural components in the AWS Cloud Development Kit (AWS CDK).

AWS CDK is a software development framework to model and provision your cloud application resources in some popular programming languages. Within AWS CDK, jsii enables the authoring and maintenance of “CDK constructs” in the TypeScript language, and in turn generates those same constructs for other languages. Jsii creates type-annotated bundles from TypeScript modules and then auto-generates idiomatic libraries (or packages) in a variety of target languages. As detailed in jsii’s runtime architecture, the generated types in these target languages proxy the calls to an embedded JavaScript VM, effectively allowing the jsii modules to be “written once and run everywhere.” Due to the performance of the hosted JavaScript engine and the marshaling costs, jsii modules are a good fit for generating development and build toolchains like the AWS CDK, but are not ideal for performance-sensitive applications or use cases.

Getting started with jsii

Getting started with jsii is straightforward—create a new npm package:

npm init -y

Ensure that these additional requirements are met for changing it into a jsii module:

"main": "lib/index.js",
"types": "lib/index.d.ts",
"scripts": { "build": "jsii", "build:watch": "jsii -w", "package": "jsii-pacmak"
},
"author": { "name": "John Doe"
},
"repository": { "url": "https://github.com/aws-samples/jsii-code-samples.git"
}

Install the jsii toolchain as development dependencies into the package - jsii and jsii-pacmak.

npm install --development jsii jsii-pacmak

Add a jsii section into the package.json with the configuration for the generated dotnet, java, and python modules:

"jsii": { "outdir": "dist", "targets": { "python": { "distName": "aws-jsiisamples.jsii-code-samples", "module": "aws_jsiisamples.jsii_code_samples" }, "java": { "package": "software.aws.jsiisamples.jsii", "maven": { "groupId": "software.aws.jsiisamples.jsii", "artifactId": "jsii-code-samples" } }, "dotnet": { "namespace": "AWSSamples.Jsii", "packageId": "AWSSamples.Jsii" } }
}

This should result in a package.json similar to this jsii code sample, which also has continuous integration set up to deploy the jsii modules into package registries such as npm, PyPI, NuGet, and Maven.

 TypeScript restrictions

To keep the jsii modules compatible with all the supported languages, the jsii compiler restricts the TypeScript language features that can be used on the public API of the jsii modules being authored. This ensures that the proxy types for the public API of the module can then be generated in all of the supported languages. These restrictions and conventions are documented on GitHub and the compiler generates helpful error messages, along with instructions to resolve any violations.

To summarize, the restrictions over vanilla TypeScript include:

  • Keywords from all the supported languages being enforced as reserved words
  • jsii conventions that view TypeScript interfaces as either:
    • Behavioral Interfaces—must be prefixed with an uppercase I (similar to IFoo), or
    • Structs—must not have methods and are therefore pure data types.

As an example, let’s look at a simple HelloWorld class with two public methods—a greeter and a Fibonacci generator:

export class HelloWorld { public sayHello(name: string) { return `Hello, ${name}`; } public fibonacci(num: number) { let array = [0, 1]; for (let i = 2; i < num + 1; i++) { array.push(array[i - 2] + array[i - 1]); } return array[num]; }
}

Performance benchmarks for native vs. jsii modules

The above example library is built and published for JavaScript/TypeScript (at npm), for Python (at PyPI), for Java (at Maven Central) and for .NET/C# (at NuGet). These were compared against equivalent native implementations in the three generated languages to generate performance benchmarks.

First let’s take a quick look at the native implementations:

Python

The Python implementation is shown below, can be seen in the jsii-native-python GitHub repo, and is published to PyPI:

class HelloWorld: def say_hello(self, name): return 'Hello, ' + name def fibonacci(self, num): if num == 0: return 0 elif num == 2 or num == 1: return 1 else: return self.fibonacci(num - 2) + self.fibonacci(num - 1)

Java

The Java implementation is shown below and can be seen in the jsii-native-java GitHub repo, and is published to Maven Central:

import java.util.ArrayList;
import java.util.Arrays; public class HelloWorld { public String sayHello(String name) { return "Hello, " + name; } public int fibonacci(Integer num) { ArrayList<Integer> array = new ArrayList<>(Arrays.asList(0, 1)); for (int i = 2; i < num + 1; i++) { array.add(array.get(i - 2) + array.get(i - 1)); } return array.get(num); }
}

C#

The C# implementation is as below and can be seen in the jsii-native-dotnet GitHub repo and is published to NuGet:

public class HelloWorld
{ public string SayHello(string name) { return $"Hello, {name}"; } public int Fibonacci(int num) { var array = new List<int> {0, 1}; for (var i = 2; i < num + 1; i++) { array.Add(array[i - 2] + array[i - 1]); } return array[num]; }
}

Now let’s take a look at the benchmarking harnesses themselves, along with the benchmark results:

Python benchmarking harness (using the built-in timeit module)

jsii moduleNative module
Windows14 ms/loop117 μs/loop
macOS6.7 ms/loop69.4 μs/loop

The native Python implementation is two orders of magnitude faster than the jsii implementation.

Java benchmarking harness (using Java Microbenchmark Harness (JMH))

jsii moduleNative module
Windows12,555.941 μs/op12.838 μs/op
macOS8,788.736 μs/op4.720 μs/op

Unsurprisingly, in the case of Java, the native implementation is around three orders of magnitude faster than the jsii one.

.NET benchmarking harness (using BenchmarkDotNet)

jsii moduleNative module
Windows16,034.767 μs/op4.282 μs/op
macOS8,067.421 μs/op2.917 μs/op

And in the case of .NET/C# once again, the native implementation is around three orders of magnitude faster than the jsii one.

Potential use cases

After recently discovering jsii while looking under the hood within AWS CDK, I was intrigued with the potential this compiler has for non-CDK use cases. Also, I have for a few years been helping to maintain an open source library that converts Gregorian calendar dates into Malayalam Era calendar (which is one of the sidereal Indian calendar systems). At the time, that was only available as an npm package for JavaScript/Node.js developers, but there were open requests to get it ported into other languages like .NET/C# and Python. In an attempt to understand jsii further, I modified the original ES6 codebase into jsii-compliant TypeScript, which helped to auto-generate the calculation-heavy library into Python, Java, and C#.

For any utility libraries like the computation-heavy calendar conversion example above, where the performance overheads shown in the previous section are acceptable, developing them once in jsii-compliant TypeScript and then getting them published for multiple languages can be a good productivity booster, especially today when polyglot microservices constituting a larger distributed application are commonplace.

Support for more languages

Similar to AWS CDK, the jsii library is developed as an open source library, and the community can and does influence the development of new features. The design goals for the library are documented in the jsii Design Tenets, and there is an established process as well as a handbook for adding support for more languages to be auto-generated from the TypeScript source. With enough demand and contributions from the community, it is not far-fetched to think that other popular languages like Go, Rust, Ruby, etc. will also be supported in the future.

Conclusion

If you need to build and maintain software libraries in multiple different programming language ecosystems, including TypeScript, and if their performance needs are within what the jsii-generated libraries will provide, get started using jsii on your TypeScript libraries and auto-generate Python, Java, and .NET versions today.