Skip to content

Interop via @proxy

Andrew Lind edited this page Sep 17, 2026 · 4 revisions

A native C++ class can be consumed (you hold a handle and call its methods) or produced (you write a subclass of it). hxcpp does the first directly, but it cannot make a Haxe class subclass a non-hxcpp C++ base — a managed, garbage-collected object can't also be a foreign C++ object (the same reason JNI never lets a Java class extends a native one). @proxy covers both: a glue type that is never emitted, identified by a mandatory fully-qualified native class name that must match a declared extern.

Consume — @proxy(...) abstract Name(cpp.Pointer<T>)

Transparent glue: every reference transpiles as the native type, so calls pass straight through. The stub bodies exist only so hxcpp type-checks.

@:include("native.h") @:native("ns::INative") @:structAccess
extern class INative { public function Run():Int; }

@proxy("ns::INative")
abstract Handle(cpp.Pointer<INative>) {
  public function Run():Int { return 0; }   // stub — Hatchet dispatches to the extern
}
// a field `h:Handle` lowers to `ns::INative* h`, and `h.Run()` to `h->Run()`

Produce — @proxy(...) abstract class Name

A base your code subclasses: the proxy itself is not emitted, but extends Name becomes : public ns::IBase and a super(...) call routes to the native constructor. This is the only way to subclass a native C++ base.

@:include("native.h") @:native("ns::IBase") @:structAccess
extern class IBase {}

@proxy("ns::IBase")
abstract class Base {
  public function new(id:Int) {}
  public abstract function OnTick():Void;
}

class Thing extends Base {             // -> class Thing : public ns::IBase
  public function new() { super(7); }  // ->   : ns::IBase(7)
  public function OnTick():Void {}
}

Rules

Each a hard error otherwise:

  • the fully-qualified native class name is a mandatory string argument (@proxy("ns::IBase"));
  • it must name a type declared extern with a matching @:native (that extern also supplies the @:include pulled into emitted subclasses);
  • it applies only to an abstract newtype (consume) or an abstract class (produce).

A proxy may share its leaf name with an unrelated native type — e.g. a ui.Vertex consume proxy (→ ui::Vertex*) alongside a gfx.Vertex value struct (@:native("gfx::Vertex") extern typedef), with a module importing both. That is safe: an unqualified Vertex in your own code follows Haxe's normal import resolution (qualify it — gfx.Vertex — when you mean the other one), but the member types of gfx's own typedefs (Mesh.vertices:Array<Vertex>, Line.a:Vertex) always resolve in gfx, so indexing, field access, and nested struct literals keep the struct's value semantics rather than picking up the proxy's pointer. Types recovered from a C++ spelling (a vector element, a map value) are likewise matched on the fully-qualified name. See Types & Nullability.

For a produce proxy, keep the Haxe member surface within what the native base actually exposes to subclasses (C++ protected/public): a field that is private in the native base would type-check under hxcpp but be rejected by the C++ compiler in an emitted subclass.


See Metadata for the full metadata split and the extern keyword.

Clone this wiki locally