Haxe Conditional Compilation: Using -D Defines to Target Platforms and Toggle Features
Use Haxe's -D flag to toggle code paths at compile time. This guide covers guarding code with #if defined(...), passing defines in build.hxml or CLI, forcing clean rebuilds, and verifying the output on JavaScript, C++, and Neko targets.
11 Sept 2026, 10:50 UTC

Desired Outcome
Compile a single Haxe codebase into multiple platform-specific binaries or feature variants by defining compile-time constants with the -D flag. This lets you include or exclude entire code paths—such as platform-specific APIs, debug logging, or optional modules—without maintaining separate source trees.
Prerequisites
- Haxe 4.2 or later (tested with 4.3.6; older versions may lack full macro expression support)
- A target toolchain installed:
hxcppfor C++, Node.js for JavaScript, or Neko VM - A build file (
build.hxml) or IDE project that invokes the Haxe compiler - Basic familiarity with
#if/#else/#endconditional compilation syntax
Procedure
1. Guard Code Sections with #if defined(...)
Wrap platform-specific or optional logic in conditional blocks. Place this in a module that gets compiled (e.g., src/Platform.hx):
class Platform {
public static function init() {
#if defined(windows)
trace("Running on Windows");
sys.FileSystem.createDirectory("C:\\AppData\\MyApp");
#elseif defined(linux)
trace("Running on Linux");
sys.FileSystem.createDirectory("/var/lib/myapp");
#elseif defined(js)
trace("Running in browser");
js.Browser.window.localStorage.setItem("init", "true");
#else
trace("Unknown platform");
#end
}
#if defined(debug)
public static function debugLog(msg:String) trace("[DEBUG] " + msg);
#else
public static function debugLog(msg:String) {}
#end
}The defined(...) checks evaluate at compile time. Code inside inactive branches is completely omitted from the generated output.
2. Pass Defines via -D in Your Build Command
Add one or more -D flags when invoking Haxe. In a build.hxml file:
-cp src
-main Main
-D windows
-D debug
-js bin/main.jsOr on the command line (run from the project root):
haxe -cp src -main Main -D linux -D no-audio -cpp bin/cppEach -D name creates a define named name with an empty value. You can also assign values: -D version=1.4.2 makes defined(version) true and #if (version == "1.4.2") evaluable.
3. Rebuild Cleanly
Because build tools (especially hxcpp) cache object files, a stale cache can hide the effect of a new define. Force a clean rebuild:
# For hxcpp targets
haxe build.hxml -clean
# Or manually remove the target output directory
rm -rf bin/cpp
# then rebuild
haxe build.hxmlRun this step after adding, removing, or changing any -D flag.
4. Verify the Generated Output
Inspect the compiler's intermediate or final output to confirm the correct branch was selected.
For JavaScript targets: Open bin/main.js and search for the guarded strings. With -D js -D debug you should see "Running in browser" and the debugLog function body. With -D js only, debugLog should be an empty function.
For C++ targets: Check bin/cpp/Platform.cpp (or the generated .cpp file for your class). The inactive #if branches will not appear in the translated C++ code at all.
Expected Checks
- Compile-time verification: Run
haxe --display @build.hxml(Haxe 4.3+) to see the resolved defines and target without compiling. - Runtime behavior: Execute the compiled program on each target. With
-D debug,Platform.debugLog("test")should print; without it, the call should be a no-op with zero overhead. - Binary size comparison: Build once with
-D debugand once without. The debug build will be larger due to retained logging strings and function bodies. On a typical C++ target, expect a 5–15% size difference for moderate logging. - Define presence in generated code: For JavaScript, grep the output:
grep -n "debugLog" bin/main.js. The line count and function body reveal whether the define was honored.
Recovery Options
Stale Cache Hides New Define
If the output doesn't reflect a new -D flag, delete the target's build directory (bin/cpp, bin/js, etc.) and rebuild. For hxcpp, also clear ~/.hxcpp/cache if you've changed compiler versions.
Define Not Recognized on a Target
Some targets ignore certain defines. JavaScript (-js) respects all -D flags passed to the Haxe compiler. Neko (-neko) does the same. However, if you're using a higher-level tool (e.g., OpenFL, Lime, or a custom build script), that tool may filter or rename defines before invoking Haxe. Check the tool's documentation for its define-passing convention (often -Ddefine=value or a separate defines section in its config).
Complex Macro Expressions Fail on Older Haxe
Expressions like #if (defined(a) && !defined(b)) require Haxe 4.0+. If you must support 3.4, split into nested #if blocks or use a macro function to compute a single derived define at compile time.
Practical Example: Feature Toggle for Audio
Suppose you ship a game with optional audio. Guard the audio initialization:
class AudioManager {
public static function init() {
#if defined(audio)
// Heavy audio engine setup
SoundSystem.initialize();
#end
}
public static function play(sound:String) {
#if defined(audio)
SoundSystem.play(sound);
#end
}
}Build the full version: haxe -cp src -main Main -D audio -cpp bin/full
Build the lightweight version: haxe -cp src -main Main -cpp bin/lite (no -D audio)
The lite binary excludes the entire audio engine, reducing download size and memory footprint. Verify by comparing ls -lh bin/full/Main bin/lite/Main and confirming the lite binary lacks SoundSystem symbols (nm bin/lite/Main | grep -i sound returns nothing).
Limitations
-Dflags are global for the compilation unit. You cannot scope a define to a single module without using separate compilation steps.- Conditional compilation happens before dead-code elimination. If a define enables a class that is never instantiated, the class may still appear in the output unless the target's DCE pass removes it (JavaScript and C++ targets do this; Neko does not).
- Defines with values (
-D key=value) are strings. Numeric comparison requires#if (Std.parseInt(key) > 5)inside a macro, not in plain#if.
Quick Verification Checklist
- Run
haxe --display @build.hxmland confirm thedefineslist matches your intent. - After a clean build, search the generated target code for a string that only exists in one conditional branch.
- Execute the binary on the target platform and observe the behavioral difference (logging appears/disappears, feature works/is absent).
- Compare artifact sizes between define-enabled and define-disabled builds.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.