Native types which are passed by value can be used in Haxe via extern classes annotated with the @:cpp.ValueType metadata.
struct Foo {
int bar;
};
@:semantics(value) @:cpp.ValueType extern class Foo { var bar : Int; function new():Void; }
You can then construct and use these objects like you would any other Haxe class. However, unlike normal Haxe classes, these are passed by value instead of reference.
function baz(o:Foo) { o.bar = 10; } function main() { final f = new Foo(); baz(f); trace(f.bar); // prints 0, not 10 }
Important: Value Semantics
If you annotate an extern class with
@:cpp.ValueTypeyou must also annotate it with@:semantics(value). Failure to do so will result in compiler error (CPP0001).
Extern enum abstracts with the cpp.ValueType metadata can be used to represent native enums.
enum Colour {
Red,
Hreen,
blue
};
@:semantics(value) @:cpp.ValueType extern enum abstract Colour(Int) { var Red; var Green; var Blue; }
Value type externs can quite often be placed on the stack, avoiding GC allocations. But for some Haxe features, such as closures, they are required to be promoted to the GC heap to behave as expected. This may not be desirable and the stack only flag allows you to forbid this GC promotion, trying to use a stack only extern type in a way which requires it to be promoted to the heap will result in a compiler error (CPP0011).
@:semantics(value) @:cpp.ValueType({ flags : [ StackOnly ] }) extern class Foo { var bar : Int; function new():Void; } function main() { final f = new Foo(); // CPP0011 - `f` is captured in closure `c` which requires it to be promoted to the heap, but it is marked as stack only. final c = () -> { f.bar = 10; } f(); trace(f.bar); }
The copy constructor and copy assignment operators of the native type are used when creating values. If the externed type has these functions deleted then compilation will fail at the C++ stage. Standard class construction is used instead of regions of memory set to zero, so any default values or constructors on the extern class will be correctly assigned and invoked.
struct Point {
double x = 7;
double y = 27;
};
@:semantics(value) @:cpp.ValueType extern class Point { var x : Float; var y : Float; public function new():Void; } function main() { final p = new Point(); trace(p.x, p.y); // 7, 27 }
The externed type will always have its destructor called if it's non-trivial. If the type lives on the stack, then it's through the usual C++ RAII mechanism. If a type with a non-trivial destructor is promoted to the heap, a finaliser is added to the promoted object to ensure the destructor is called. This does mean that the destructor is now called non-deterministically, since it relies on the GC collecting the object.
The == and != operators on the externed type are used for comparison between value types.
Local variables which are value types are not nullable, declaration without initialisation results in a compiler error (CPP0005) and trying to assign a null value type to a local variable results in a runtime exception. Value type local variables are allowed to be null if they are explicitly wrapped in Null<>.
Value types in class or interface fields as well as enums act like normal objects, i.e. they are nullable and are null by default unless instantiated in the constructor.
Value types stored in Haxe containers such as arrays, maps, vectors, and lists are individually boxed. They are not stored in contiguous memory and attempting to get a pointer to the underlying storage for this purpose is not valid.
This is done to avoid all the complexities of copy constructors and in-place construction which would be needed to support the growing and shrinking of most Haxe containers.
Templated types can be represented by Haxe generic argument with some important limitations. Since C++ templates are compile time polymorphism and Haxe generic arguments are runtime polymorphism (with type erasure), the compiler must be able to resolve a generic argument to a concrete type. If it's unable to do this it will generate a compiler error (CPP0010). The resolved types must also not be GC objects, i.e. they cannot be Haxe strings, enums, or classes, only numeric types and other native marshalling types.
@:semantics(value) @:cpp.ValueType extern class Foo { static function print(v:T):Void; } function bar<T>(v:T) { Foo.print(v); } @:generic function baz<T>(v:T) { Foo.print(v); } function main() { Foo.print(100); // Ok, can resolve T to Int. Foo.print("test"); // CPP0003, resolved generic argument is an invalid type. bar(100); // CPP0010, cannot resolve generic argument. baz(100); // Ok, @:generic produces a specialisation for each parameter which allows the the parameter to be resolved. }
Value types automatically decay to pointers and references, meaning you do not need to deal with the pointer types in the cpp package to get addresses.
struct Foo {
static void bar(Foo* f);
};
@:semantics(value) @:cpp.ValueType extern class Foo { function new():Void; static function bar(f:haxe.extern.AsVar<Foo>):Void; } function main() { final f = new Foo(); Foo.bar(f); // Address of `f` is automatically passed in. }
Despite not being needed in most situations, value types are compatible with the existing pointer types in the cpp package (cpp.Pointer, cpp.RawPointer, cpp.Star, and cpp.Reference) and can be used to explicitly represent pointers to value types.