Haxe is a programming language that can be transpiled to many other languages thanks to the powerful Haxe compiler. Because of this very nature, Haxe is really versatile and the Haxe developer spectrum is really wide. Developers use Haxe to develop games, web apps, mobile apps, desktop apps, servers, tools, etc. Therefore, when authoring a Haxe library, ideally it should cover as many targets as possible. So that everyone would be able to use it. Here I would like to list a few approaches to utilize Haxe and its targets to write truly cross-target code, using a work-in-progress Big Integer library I have been working on recently.

Silver bullet: Write the entire implementation in Haxe

When code is written in pure Haxe, it is guaranteed to work on every targets (provided that it doesn’t use any target-specific APIs). Result-wise, this is the ideal solution. Due to the single code base, we can guarantee the program behaviour is consistent across targets. Also, this approach doesn’t has any external dependency, meaning that Haxe user will only need to install the code via haxelib (the Haxe library manager). Then the compiled program will just work, without additional setup.

However, this approach is very time-consuming. I am using the BigInteger.js library as the reference implementation. It contains about 1500 lines of code and it is utilizing a lot of JS-specific tricks. That means I can’t easily port it to Haxe without many changes. It would probably cost me weeks to completely port it to Haxe. Someone would want to do this ultimately. But I want to kickstart quickly in a hope that it would facilitate a Haxe RSA implementation.

Exploit the targets

Instead of writing the whole thing from scratch with Haxe. I decided to harvest the strength of each target. This is a much simpler approach yet very pragmatic. In order to do that, I first defined an API, then implemented the details for each target, separately. For example, for my Big Integer library, it would have a constructor plus a set of arithmetic operations. A simplified version would look like the following. (Note that I am using abstract instead of interface . Because it supports implicit cats, operator overload and doesn’t have dynamic dispatch overhead which is present in interface )

abstract BigInt(Impl) { // store commonly used values as a static var public static final ONE:BigInt; // create a big int from string @:from public inline static function fromString(v:String):BIgInt; // max of two values public inline static function max(a:BigInt, b:BigInt):BigInt; // addition, with operator overload @:(A+B) public inline function add(other:BigInt):BigInt; // string representation public inline function toString():String; }

After defining the API, the next thing is the implementations. There are a few ways to do so depending on the target language’s characteristics, feature set and ecosystem. The following is an excerpt from the actual work I have done. Full versions can be found in the project repo.

Python: By default integer has unlimited size

Since python’s integer is “big integer” already, I can simply map all the API operations to standard integer operations. It is going to work right out of the box.

abstract Impl(Int) from Int to Int { public static final ONE:Impl = 1; public static inline function fromString(v:String):Impl return UBuiltins.int(v); public inline static function max(a:Impl, b:Impl):Impl return a > b ? a : b; public inline function add(other:Impl):Impl return this + other; public inline function toString():String return UBuiltins.str(this); }

(Note how the implementation uses standard operators and builtin methods.)

Java: BigInteger included in standard library

Java has the java.math.BigInteger class, so I could utilize it. It also works out of the box.

import java.math.BigInteger as Native; abstract Impl(Native) from Native to Native { public static final ONE:Impl = Native.ONE; public static inline function fromString(v:String):Impl return new Native(v); public inline static function max(a:Impl, b:Impl):Impl return a.max(b); public inline function add(other:Impl):Impl return this.add(other); public inline function toString():String return this.toString(); }

(Note how the implementation uses the API provided by the native Java BigInteger class.)

Javascript: Use the BigInteger.js library

On newest browsers there is a native BigInt type. But for best compatibility I chose to use a library. (and the library will utilize the native type when it is available).

abstract Impl(Native) from Native to Native { public static final ONE:Impl = Native.one; public static inline function fromString(v:String):Impl return new Native(v); public inline static function max(a:Impl, b:Impl):Impl return Native.max(a, b); public inline function add(other:Impl):Impl return this.add(other); public inline function toString():String return this.toString(); } @:jsRequire('big-integer') private extern class Native { static var one:Native; function new(v:Any); static function max(a:Any, b:Any):Native; function add(number:Any):Native; function toString():String; }

(Note how the implementation uses the library’s API)

A side note for js: Normally, in order to use js libraries, one would need a package manager such as npm or yarn. But thanks to Juraj‘s awesome library embed-js, we can instruct the Haxe compiler to download the library’s js code and embed it into the Haxe-generated js output at compile time. Making it work out of the box as well.

PHP: Use GMP extension

On PHP there is a native extension that supports big integer operations called GMP. Setting it up isn’t too hard. Just download the binary and enable the extension in php.ini .

abstract Impl(Native) from Native to Native { public static final ONE:Impl = fromString('1'); public static inline function fromString(v:String):Impl return Gmp.gmp_init(v); public inline static function max(a:Impl, b:Impl):Impl return Gmp.gmp_cmp(a, b) > 0 ? a : b; public inline function add(other:Impl):Impl return Gmp.gmp_add(this, other); public inline function toString():String return Gmp.gmp_strval(this); } @:phpGlobal extern class Gmp { static function gmp_add(a:Gmp, b:Gmp):Gmp; static function gmp_cmp(a:Gmp, b:Gmp):Int; static function gmp_init(n:Any, ?b:Any):Gmp; static function gmp_strval(a:Gmp):php.NativeString; }

(Note that this is very similar to Java except the additional setup)

Conclusion

I found this “exploit the targets” approach works very well. It is simple and effective. More importantly, it ensured extensibility because the interface definition itself is target-agnostic. For additional targets to be supported, one only needs to implement the interface accordingly, may it be a pure Haxe implementation or implemented via some libraries in the target’s ecosystem. Anyway I reassure the ideal approach would be a pure Haxe implementation. But before we have time do so this would be a pretty decent and practical compromise.

In fact there are quite a few libraries in the wild already using this method. Here are some of them: tink_http, tink_io, tink_tcp, why-ble, why-fs

This topic is pretty unique to Haxe because of it multi-target nature. Hopefully more libraries would be written in this way so that it benefits the Haxe ecosystem more!