Building a Cross‑Target Haxe Library: Core, Adapters, and Facade
Learn how to structure a Haxe library so a pure‑Haxe core runs unchanged on JavaScript, Python, and the JVM, with thin extern adapters behind conditional compilation and a single facade.
15 Jun 2026, 19:25 UTC

Requirements
The library must expose a single domain‑logic API that behaves identically when compiled to JavaScript, Python, and the JVM. All platform‑specific I/O (file access, network sockets, process spawning) must be isolated so the core remains deterministic and easy to unit‑test.
Smallest Suitable Design
We split the source into three layers:
- Core package – pure Haxe code, no
externdeclarations, nountypedblocks. This package contains the domain model, validation rules, and any algorithm that must be reproducible. - Adapter modules – one per target, selected with conditional compilation (
#if js,#if python,#if jvm). Each adapter implements a tiny interface that the core depends on (e.g.,IFileReader). The adapter bodies contain the necessaryexterncalls to the target’s standard library. - Facade – a single public class that re‑exports the core’s API and injects the appropriate adapter at compile time via a type alias or abstract.
Consumers depend only on the facade; they never import an adapter directly.
Example: File‑reading adapter
// src/core/IFileReader.hx
package core;
interface IFileReader {
function read(path:String):String;
}
// src/adapters/js/FileReader_js.hx
#if js
package adapters;
import js.NodeJS.Fs;
class FileReader_js implements core.IFileReader {
public function read(path:String):String {
return Fs.readFileSync(path, {encoding:'utf8'});
}
}
#end
// src/adapters/python/FileReader_py.hx
#if python
package adapters;
import python.File;
class FileReader_py implements core.IFileReader {
public function read(path:String):String {
return File.readAllText(path);
}
}
#end
// src/adapters/jvm/FileReader_jvm.hx
#if jvm
package adapters;
import java.nio.file.Files;
import java.nio.file.Paths;
class FileReader_jvm implements core.IFileReader {
public function read(path:String):String {
return new String(Files.readAllBytes(Paths.get(path)), java.nio.charset.StandardCharsets.UTF_8);
}
}
#end
The core uses the interface:
// src/core/Document.hx
package core;
import adapters.FileReader_js; // placeholder; actual type is resolved by facade
class Document {
private var reader:IFileReader;
public function new(reader:IFileReader) {
this.reader = reader;
}
public function load(path:String):String {
// core logic stays the same on every target
var raw = reader.read(path);
return raw.trim().toUpperCase();
}
}
Trust and Data Boundaries
Any value that crosses from an adapter into the core must be validated. For example, if an adapter returns a string that could contain null bytes, the core should reject or sanitize it before further processing. Conversely, core objects must never be passed directly to an extern function; they should be converted to primitive types or plain objects first.
Operational Checks
To guarantee that the design holds, we automate the following in CI:
- Compile the core to every target – a single
build.hxmlis invoked three times with different-Dflags. - Run the shared test suite – the same unit‑test source is executed under Node.js, CPython, and the JVM.
- Smoke test each adapter – a small integration exercise calls the adapter with a real or recorded native resource (e.g., a temporary file) and asserts the returned value matches expectations.
- Inspect dead‑code elimination output – after compilation we check the generated
.js,.py, or.classfiles to ensure the adapter entry points are present and not stripped.
Sample CI script (bash)
#!/usr/bin/env bash
set -euo pipefail
HAXE_VERSION="4.3.0" # pin the compiler version
# 1. JavaScript
haxe -D js -main src/core/TestSuite.hx -js out/js/test.js
node out/js/test.js
# 2. Python
haxe -D python -main src/core/TestSuite.hx -python out/python/test.py
python3 out/python/test.py
# 3. JVM
haxe -D jvm -main src/core/TestSuite.hx -java out/jvm
java -cp out/jvm TestSuite
# 4. Verify adapter presence (example for JS)
grep -r "FileReader_js" out/js/ || echo "Adapter missing in JS output"
Run this script on a CI agent with read/write access to the repository and permission to execute haxe, node, python3, and java. The biggest risk is a false‑negative if the adapter code is eliminated by dead‑code elimination; the grep step mitigates that.
Failure Modes
- Dead code elimination – if the compiler cannot see that an adapter is used (e.g., because the facade is accessed via reflection), the whole adapter may be dropped, causing runtime
ClassNotFounderrors. - Stdlib divergence – map iteration order differs between JS (insertion order) and older JVM versions; integer width is uniform but regex flags (
g, m) are not available on Python’sremodule without extra work. - Extern drift – the
@:nativemetadata or generated extern signatures can change between Haxe releases, breaking the adapter silently. - Async/model mismatch – JS uses Promises, Python uses
asynciocoroutines, and the JVM may rely onCompletableFuture. If the core starts to depend on a specific concurrency primitive, the thin‑adapter assumption fails.
When the Design Must Change
Re‑evaluate the split if any of the following occurs:
- A target requires a fundamentally different async or concurrency model that cannot be hidden behind a synchronous
IFileReader-like interface. - The adapter surface grows beyond a few simple wrappers (e.g., you need to expose complex lifecycle objects, thread pools, or native event loops). In that case, move the target‑specific logic into its own implementation and keep only a narrow interface (or a set of golden test vectors) shared across targets.
- You need to compile to a new target whose standard library lacks the primitives your adapters rely on, forcing you to rethink the boundary between core and adapter.
When any of these conditions appear, create a new package per target that implements the shared interface, and keep the core free of #if blocks.
Practical Verification Steps
- Pin the Haxe version in
haxelib.jsonor your CI container. - Run the build script above on a clean checkout.
- Confirm that the core package (
src/core/**) contains no#if js,#if python, or#if jvmlines (search withgrep -R "#if" src/core). - Run the test suite on each target and compare the output to a stored golden file; any diff signals semantic drift.
- After each build, examine the generated artifacts for the adapter symbols (e.g.,
FileReader_jsin the JS bundle) to ensure they are retained.
If all checks pass, you have confidence that the library shares deterministic logic while keeping platform‑specific code safely isolated.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.