Skip to content

[cpp] Marshalling Extern Types #11981

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 196 commits into
base: development
Choose a base branch
from

Conversation

Aidan63
Copy link
Contributor

@Aidan63 Aidan63 commented Feb 6, 2025

Another long one, so make sure you're sitting comfortably.

Corresponding hxcpp PR: HaxeFoundation/hxcpp#1189

The Problem

Working with the current interop types comes with many pitfalls which are not immediately obvious, these include.

  • Externed value types (cpp.Struct / cpp::Struct) do not work as expected when captured in a closure. If you mutate one of these captured value types you are mutating a copy, not the object referenced by the variable name.
  • These struct types only really work with C structs, not C++ structs and classes which might have all sorts of interesting constructors, destructors, assignment operators, etc. Structs are compared using memcmp and copies are made using memcpy, so if you have a C++ class which requires copy constructors, copy assignment, or others to work correctly, things will go wrong in ways which are painful to debug.
  • Leaking objects is very easy as destructors are not consistently called. If you place one of these structs in a class field its destructor will never be called.
  • Value types stored in classes will be initialised to zero, not have any default constructor invoked, so any default values or setup code will not be invoked.
  • It’s very easy to write code which does different things depending on the debug level and optimisations enabled by the compiler. You can easily find yourself modifying a copy of a struct in a temporary variable the compiler created, creating very hard to track down bugs.
  • Common C/C++ patterns, such as pointers to pointers, are hard to extern without lots of boilerplate and glue code wrangling.
  • All extern classes are considered native by the generator, so if you want to extern a custom hx::Object sub class some of the necessary GC code isn’t generated for the generational GC.
  • Many other things!

In short, the current interop handlers mostly work with basic c structs in local variables, but if you want to interop directly with C++ classes, you’re going to have a painful time in anything but the most basic cases.

If you just want to see a quick example of it all in action here's a gist which will compile on Windows and use DXGI to print out all displays connected to the first graphics adapter. DXGI uses pointers to pointers, pointers to structs, pointers to void pointers, and other C++ concepts which have been very difficult to map onto haxe in the past. But hopefully you'll agree that it looks like pretty "normal" haxe.

https://gist.github.com/Aidan63/07364c227335f02fbe50b9c9576f7544

New Metadata

In my mind there are three categories of things you might want to extern, native value types, native pointer types, and "managed" (custom hx::Object sub classes) types. This merge introduces three new bits of metadata to represent these categories and solve the above issues.

cpp.ValueType

Using the @:cpp.ValueType metadata on an extern class will cause it to be treated as a value type, so copies will be created when passing into functions, assigning to variables, etc, etc.

@:semantics(reference)
@:cpp.ValueType({ type : 'foo', namespace : [ 'bar', 'baz' ] })
extern class Foo {
	var number : Int;

	function new() : Void;
}

function main() {
	final obj  = new Foo();
	final copy = obj; // creates a copy
}

I've chosen the metadata to take a struct which currently supports a type field for the underlying native type name and namespace which must be an array of string literals for the namespace the type is in. If type is omitted then the name of the extern class is used, if namespace is omitted then the type is assumed to be in the global namespace.

Using this metadata provides several guarantees old struct types. First it behaves how you'd expect when captured in a closure.

function main() {
	final obj  = new Foo();
	final func = () -> {
		obj.number = 7;
	}

	func();

	trace(obj.number); // 7 will be printed.
}

Destructors are guaranteed to be called. When a value type is captured in a closure, stored in a class field, enums, anons, or any other GC objects it is "promoted" to the GC heap and the holder class its contained within registers a finaliser which will call the destructor.

Operators on the defined native type are always used, no memcmp or memcpy. Copy constructors, copy assignment, and standard equality operators are always used no matter the case.

The same sort of null checks are performed with references to these value types as standard haxe classes so you will get standard null exceptions instead of the program crashing with a memory error.

The nullability of these values types is unfortunately a bit odd... If you have a explicitly nullable TVar value type then its always promoted and can be null. But a null value type doesn't make much sense so I've disallowed value type variable declarations with no expression or with a null constant. Trying to assign a null value to a stack value type will result in a runtime null exception. Since value types in class fields and the likes are always promoted they are null if uninitialised. Ideally value type externs could have the same "not null" handling as core type abstracts, but that doesn't seem possible.

Interop with the existing pointer types is also provided as well implicit conversions to pointers on the underlying types for easier interop.

This value type metadata is also supported on extern enum abstracts. Historically externing enums have been a bit of a pain but it works pretty nicely now.

@:semantics(reference)
@:include('colour.hpp')
@:cpp.ValueType({ type : 'colour' namespace : [ 'foo' ] })
private extern enum abstract Colour(Int) {
    @:native('red')
    var Red;

    @:native('green')
    var Green;

    @:native('blue')
    var Blue;
}

cpp.PointerType

Using the @:cpp.PointerType metadata on an extern class will cause it to be treated as a pointer, this metadata supports the same fields as the above value type one.

Extern classes annotated with the pointer type metadata cannot have constructors as they are designed to be used with the common C/C++ pattern of free functions which allocate and free some sort of opaque pointer, or the pointer to pointer pattern.

E.g. the following native API could be externed and used as the following.

namespace foo {
	struct bar {};

	bar* allocate_bar();
	void free_bar(bar*);

	struct baz {};

	void allocate_baz(baz** pBaz);
	void free_baz(baz* baz);
}
@:semantics(reference)
@:cpp.PointerType({ type : 'bar', namespace : [ 'foo' ] })
extern class Bar {}

@:semantics(reference)
@:cpp.PointerType({ type : 'baz', namespace : [ 'foo' ] })
extern class Baz {}

extern class Api {
	@:native('allocate_bar')
	function allocateBar():Bar;

	@:native('free_bar')
	function freeBar(b:Bar):Void;

	@:native('allocate_baz')
	function allocateBaz(b:haxe.extern.AsVar<Baz>):Void;

	@:native('free_baz')
	function freeBaz(b:Baz):Void;
}

function main() {
	final bar = Api.allocateBar();

	Api.freeBar(bar);

	final baz : Baz = null;

	Api.allocBaz(baz);
	Api.freeBaz(baz);
}

The pointer to pointer pattern which is pretty common is quite difficult to extern without custom C++ glue code, but the new pointer type externs understand this pattern and can be converted to a double pointer of the underlying type as well as a pointer to a void pointer which is also seen in many places.

Internally pointer types and value types are treated almost identically so most of the previous points apply here as well, the main exceptions being that promoted pointers don't have finalisers assigned and that null is always an allowed value.

cpp.ManagedType

When you want to extern a custom hx::Object subclass then this is the metadata to use as it ensures the write barriers are generated for the generational gc. Like the above two metadata it supports the type and namespace fields.

namespace foo {
	struct bar : public ::hx::Object {};
}
@:cpp.ManagedType({ type : 'bar', namespace : [ 'foo' ] })
extern class Bar {
	function new() : Void;
}

In the above sample Bar will be generated as ::hx::ObjectPtr<bar> in most cases.

There is one extra field to the managed type, flags, which is expected to be an array of identifiers and currently there is one flag, StandardNaming. If in C++ you use the hxcpp class declaration macro to create a custom subclass with the same naming scheme as haxe generated classes then this flag is designed for that.

HX_DECLARE_CLASS1(foo, Bar)

namespace foo {
	struct Bar_obj : public ::hx::Object {};
}
@:cpp.ManagedType({ type : 'Bar', namespace : [ 'foo' ], flags : [ StandardNaming ] })
extern class Bar {
	function new() : Void;
}

In the above case Bar will be used instead of the manual ::hx::ObjectPtr wrapping but Bar_obj will be used when constructing the class.

Implementation Details and Rational

Marshalling State

Value and pointer types are represented by the new TCppNativeMarshalType enum which can be in one of three states, Stack, Promoted, or Reference. This is the key to working around optimiser issues, capturing, and some interop features. All TCppNativeMarshalType fields are given the promoted state and TVars can be given any three of the states. Any TLocal to a native marshal type is given the reference state. How TVars are given their state is important, variables allocated by the compiler are given the reference state, only variables typed by the user are given one of the stack (uncaptured) or promoted (captured or nullable) state. This means we dodge the issue with cpp.Struct where you could be operating on a copy due to compiler created variables.

TLocals of the reference state are generated with the new cpp::marshal::Reference<T> type which holds a pointer to a specific type and is responsible for any copying, promotion, checking, and just about everything. For the value type case it's T will be a pointer to the value type, and for pointer types will be a pointer to the pointer.

Semantics

You are required to put the @:semantics(reference) metadata on an extern class when using the value or pointer type metadata, this does feel like a bit of a bodge... I was initially hoping that the value semantic would be what was needed, but tests start to fail when the analyser is enabled with value semantics. Maybe I'm just misinterpreting what these semantics are actually used for. With the reference semantics the tests do pass with the analyser, but from a quick glace that appears to be because many optimisations are disabled on types with that semantic meta...

Compiler Error Numbers

There are several errors you may now get if you try and do things wrong (invalid meta expression) or which are not supported (function closures) instead of vague C++ errors. In these cases I've given them distinct error numbers in the CPPXXXX style, similar to MSVC and C# errors. I plan on documenting these since they're things users might naturally cause as opposed to internal bugs, so I thought it might be nice to give then concrete numbers for easier searching.

image

Scoped Metadata

I can never remember the names of the C++ specific metadata and end up sifting through the dropdown list every time, so I decided to prefix these ones with cpp. to make it easier.

Metadata Style

I wanted to avoid re-using the @:native metadata for the extern classes as its already pretty common to do stuff like @:native("::cpp::Struct<FooStruct>") so by having a type and namespace field I wanted to make it clear it should be just the type, nothing else. Also with this we can prefix :: to the type / namespace combo to avoid any relative namespace resolution issues.

Eager Promotion

Due to the very dynamic nature of hxcpp's function arguments and return types there are many places where value types which could be on the stack have to be promoted to satisfy the dynamic handling. With my callable type PR this should be solvable.

Future Stuff

Closures

Currently trying to use a function closure of a value or pointer type will result in a compilation error, but now that the variable promotion stuff is in place it should be possible to generate closures which capture the object to support this. Again I wouldn't want to do this until the callable change is in since that will greatly simplify things.

Arrays and Vectors

Value types stored in contains such as arrays are in their promoted state, not a contiguous chunk of memory which I originally wanted. Preserving C++ copying / construction semantics with haxe's resizable array looked to be a massive pain so I decided not to.
I do think having haxe.ds.Vectors of value types be contiguous should be possible and open up more easier interop possibilities.

Un-dynamicification

Lots of the cpp std haxe types have a Dynamic context object which is then passed into C++ where its cast to a specific type. With the managed type meta we should be able to "un-dynamic" a lot of the standard library implementation.

Closing

I'm sure there's stuff I've missed but this seems to be much more consistent in behaviour and nicer to use than the existing interop types, I've also added a bunch of tests on the hxcpp side to capture all sorts of edge cases I came across. I will also try and write some formal documentation for all this to encourage this over the existing types.

@kevinresol
Copy link
Contributor

Thanks for looking into it! When it is merged, would love to see a rebase so I can test out the binary built from this PR

@Aidan63
Copy link
Contributor Author

Aidan63 commented Mar 17, 2025

I've merged dev into this branch so new builds should have the fix.

@kevinresol
Copy link
Contributor

kevinresol commented Mar 18, 2025

Okay I can compile the existing code with the haxe binary now. So I can actually start testing this out.

I just found that on the cpp side the marshalling wrapper does not seem to be compatible with cppia:

Host.hx

@:headerCode('enum MyEnum { X = 3, Y = 27 };')
class Host {
	public static function main() {}

	public function new(type:MyEnum) {}

	static function getValue():MyEnum {
		return MyEnum.X;
	}
}

@:semantics(reference)
@:cpp.ValueType({type: 'MyEnum', namespace: []})
extern enum abstract MyEnum(Int) to Int {
	final X;
	final Y;
}

build.hxml

--main Host
--cpp bin
-cp src
-D scriptable

generated cpp

::cpp::marshal::ValueType< ::MyEnum > Host_obj::getValue();

static void CPPIA_CALL __s_getValue(::hx::CppiaCtx *ctx) {
  ctx->returnInt(Host_obj::getValue());
}

Outcome:
cpp compiler complains that it can't convert marshalled enum to an int, for a cppia function returning the enum

Error: ./src/Host.cpp:84:16: error: no viable conversion from '::cpp::marshal::ValueType< ::MyEnum>' to 'int'
   84 | ctx->returnInt(Host_obj::getValue());

also it can't convert an int to the marshalled enum for a cppia function arg:

@Aidan63
Copy link
Contributor Author

Aidan63 commented Mar 21, 2025

This should now be working, or, atleast, the marshalling tests now compile with the scriptable define which is a good start.

@kevinresol
Copy link
Contributor

hmm, I am still unable to compile the code snippet in my previous post.

Got this:

Error: In file included from ./src/Host.cpp:2:
In file included from /Users/kevin/haxe/haxelib/hxcpp/git/include/hxcpp.h:357:
/Users/kevin/haxe/haxelib/hxcpp/git/include/cpp/Marshal.h:618:63: error: cannot initialize a member subobject of type 'MyEnum' with an rvalue of type 'int'
  618 | inline cpp::marshal::ValueType<T>::ValueType(TArgs... args) : value( std::forward<TArgs>(args)... ) {}
      |                                                               ^      ~~~~~~~~~~~~~~~~~~~~~~~~~
./src/Host.cpp:104:65: note: in instantiation of function template specialization 'cpp::marshal::ValueType<MyEnum>::ValueType<int>' requested here
  104 |  _HX_SUPER ? ((Host_obj*)ctx->getThis())->Host_obj::__construct(ctx->getInt(sizeof(void*))) : ((Host_obj*)ctx->getThis())->__construct(ctx->getInt(sizeof(void*)));
      |                                                                 ^
./src/Host.cpp:106:51: note: in instantiation of function template specialization '__script_construct_func<false>' requested here
  106 | ::hx::ScriptFunction Host_obj::__script_construct(__script_construct_func,"vi");
      |                                                   ^
1 error generated.
 ERROR  (unknown position)

   | Error: Build failed

Does it happen on your side as well?

@Aidan63
Copy link
Contributor Author

Aidan63 commented Mar 23, 2025

Yep, it was happening on my side as well. I got the marshalling tests compiling with the scriptable flag and forgot to check your original sample. There's a fair bit of duplication in the cppia support code generation which could probably do with being cleaned up...

@kevinresol
Copy link
Contributor

I guess there is another missing bit for member functions returning a value type:

@:headerCode('enum MyEnum { X = 3, Y = 27 };')
class Host {
	public static function main() {}

	public function new(type:MyEnum) {}

	static function staticGetValue(input:MyEnum):MyEnum {
		return MyEnum.X;
	}

	function getValue(input:MyEnum):MyEnum {
		return MyEnum.X;
	}
}

@:semantics(reference)
@:cpp.ValueType({type: 'MyEnum', namespace: []})
extern enum abstract MyEnum(Int) to Int {
	final X;
	final Y;
}
Error: In file included from ./src/Host.cpp:2:
In file included from /Users/kevin/haxe/haxelib/hxcpp/git/include/hxcpp.h:357:
/Users/kevin/haxe/haxelib/hxcpp/git/include/cpp/Marshal.h:618:63: error: cannot initialize a member subobject of type 'MyEnum' with an rvalue of type 'int'
  618 | inline cpp::marshal::ValueType<T>::ValueType(TArgs... args) : value( std::forward<TArgs>(args)... ) {}
      |                                                               ^      ~~~~~~~~~~~~~~~~~~~~~~~~~
./src/Host.cpp:110:10: note: in instantiation of function template specialization 'cpp::marshal::ValueType<MyEnum>::ValueType<int>' requested here
  110 |                 return __ctx->runInt(__scriptVTable[1]);
      |                        ^
1 error generated.
 ERROR  (unknown position)

I guess the generated runInt should be runObject?

@Aidan63
Copy link
Contributor Author

Aidan63 commented Mar 24, 2025

yes, that one should be fixed now as well. I've done some cleanup of the cppia scaffolding generation so that should hopefully be the last of the issues (last famous words...).

@kevinresol
Copy link
Contributor

kevinresol commented Mar 26, 2025

Edited:
The original comment was a bit messy, let me try to conclude it better.

In short I would like to know how to consistently obtain a pointer to a ValueType, and the actual value of a ValueType. For the purpose of interop-ing with native code that expects pointer of the value type and actual value of the value type.
Because the actual promotion state in the generated native code can be different in different scenarios.

Original:
I noticed that following in the ValueType section:

Interop with the existing pointer types is also provided as well implicit conversions to pointers on the underlying types for easier interop.

How does it work if I want to get the pointer to a ValueType value regardless of its promotion state?

Besides pointer, I am also curious in how to make haxe emit code that correctly dereferences a ValueReference if it decided to produce a temp var. For example, I can see that currently the following is generated for the haxe code untyped __cpp__ ('x = {0}', p_rhs.__gd) where p_rhs.__gd is a ValueType for godot::Variant stored in a class field __gd

::cpp::marshal::ValueReference< ::godot::Variant > p_rhs1 = ( (::cpp::marshal::ValueReference< ::godot::Variant >)(p_rhs) );
return x == p_rhs1;

But it is not correct because I expect the actual value be used in the comparison, not the reference created by haxe.
Maybe we need some extra Haxe API for these kind of stuff?

@Aidan63
Copy link
Contributor Author

Aidan63 commented Mar 26, 2025

There are two ways you can deal with pointers to value types. If you have a function which accepts a pointer to a type you've externed as a value type the easiest way is to just extern that function using the existing haxe extern and C++ operator conversions will take care of it. I used this method in the directx gist in the original post. E.g.

I have a value type extern for DXGI_ADAPTER_DESC like this.

@:semantics(reference)
@:cpp.ValueType({ type : 'DXGI_ADAPTER_DESC' })
extern class DxgiAdapterDescription {
    @:native('VendorId')
    var vendorId : Int;

    @:native('DeviceId')
    var deviceId : Int;
}

and I want to use a function IDXGIAdapter::GetDesc which has the following signature, HRESULT GetDesc(DXGI_ADAPTER_DESC* pDesc), so a pointer to my value type extern. If I simply extern that function as the following,

@:native('GetDesc')
function getDesc(desc : DxgiAdapterDescription) : Int;

Then it will "Just Work" as C++ operators take care of getting a pointer to the value type passed in, no matter what the state of it is.

The alternative if you want to be a bit more explicit is to use something like cpp.Star or similar in that externed function signature.

@:native('GetDesc')
function getDesc(desc : cpp.Star<DxgiAdapterDescription>) : Int;

When you use those existing pointer interop types with value types it will use the underlying type, not the hxcpp wrapper types. i.e. cpp.Pointer<DxgiAdapterDescription> becomes ::cpp::Pointer<DXGI_ADAPTER_DESC>, not something like ::cpp::Pointer<cpp::marshal::ValueType<DXGI_ADAPTER_DESC>>.

There are more value type interop examples in the hxcpp tests. https://github.com/HaxeFoundation/hxcpp/blob/7fcb9203ae2da9e01e82877232d7cac7122ec7bb/test/native/tests/marshalling/classes/TestValueTypeInterop.hx

For variables, only those typed by the user will be given the stack or promoted state, any internal compiler generated variables will be given the reference state. This is to avoid the sort of issues you see withcpp.Struct where you can end up mutating a compiler generated variable, and therefore a copy.
That reference equality can be quite deceptive, ::cpp::marshal::ValueReference has overloaded equality operators where it will dereference the value type, it does not compare based on pointers. This means the equality operators of the native type are used.

@kevinresol
Copy link
Contributor

Thanks for the explanation, I seem to get the pointer/value part working (although I reckon it can be optimized a little bit)

For my use case I need to pass a pointer to native API, the pointer cast in Haxe works well. On the other hand I also need to be able to get the actual value for native operator overloads, which can be done via casting it to cpp.Pointer again then use its value field. There are also native API that accepts a reference, but it seems fine just passing a dereferenced value to it. Though I am not very sure about the performance implication for that.

The other topic I would like to explore is about using the Haxe generated code from native side.
I noticed that ValueType used as haxe function args are generated as cpp::marshal::Boxed<T> and return values are generated as cpp::marshal::ValueType<T>. The latter looks pretty straight forward I can just get the actual value via the value field. The former is more compilcated, consider the following Haxe example:

class Foo {
  var x:MyValueType;
  public function new(x:MyValueType) this.x = x;
}

The generated c++ will be like:

// this->x is typed as Boxed<MyValueType>
void Foo_obj::__construct(::cpp::marshal::ValueType< MyValueType > v) {
	this->x = (::cpp::marshal::ValueReference< MyValueType >)(v);
}

If I understand correctly ValueType is stack allocated and I don't get how it ends up being allocated in the Heap (for ValueReference and Boxed) during the the various casts.

One more side note: extern enum abstract annotated with ValueType is not compatible with string concatenation any more. I need to do a manual int cast so this works: trace('Hello: ${(x : Int)}

@Aidan63
Copy link
Contributor Author

Aidan63 commented Mar 27, 2025

For my use case I need to pass a pointer to native API...

I'm not sure I follow this paragraph, do you have some concrete examples?

I noticed that ValueType used as haxe function args..

Sometimes value type arguments and return types need to be boxed to work with certain haxe function "features" related to dynamic. The good news is I've worked on improving function calling in #11151 so that you only pay the dynamic cost if you're explicitly using it, so in the future value type arguments won't need to be boxed as often.

The types ::cpp::marshal::ValueType<T> and ::cpp::marshal::Boxed<T> can be considered the "storage" for value types, in the case of ValueType it will be on the stack and cleaned up when the stack unwinds, whereas Boxed lives in the GC heap. The third type ::cpp::marshal::ValueReference is a sort of "universal accessor" to a value type. I didn't want to mostly duplicate all logic for value type handling in both ValueType and Boxed, so anything you do with a value type will go through that light ValueReference wrapper which maintains value semantics, haxe null compatibility, conversions, etc, etc.

One of the conversions is an implicit conversion to a Boxed<T> which is what happens in that line.

this->x = (::cpp::marshal::ValueReference< MyValueType >)(v);

The ValueReference<T> is being assigned to the Boxed<T> field, so the implicit conversion function is invoked. This allocates a new Boxed<T> and calls the copy constructor on the dereferenced value pointer to preserve the underlying type semantics.

https://github.com/HaxeFoundation/hxcpp/blob/7fcb9203ae2da9e01e82877232d7cac7122ec7bb/include/cpp/Marshal.h#L282

Most of this should be implementation details, even if you're writing C++ glue code you shouldn't need to manually deal with the value, boxed, or reference state types (although I'm sure there are some extreme edge cases), you can just deal with standard C/C++ pointers, references, and values. Appropriate conversions should automatically happen when those type pass between haxe and C/C++.

extern enum abstract annotated with ValueType is not compatible with string concatenation any more

I'll take a look, wonder if this is a general limit of extern enum abstracts as opposed to something wrong with the C++ stuff.

@kevinresol
Copy link
Contributor

kevinresol commented Mar 27, 2025

I'm not sure I follow this paragraph, do you have some concrete examples?

So basically I am dealing with cases where the expected type is not explicit , such as c++ operator overloads.

Let's say a class X has a op overload for value type Y:

// c++
class X {
  bool operator==(const Y& y);
}

When trying to generate the comparison code in Haxe:

// haxe
// x is instance of `extern class X {}`
// y is instance of `@:cpp.ValueType extern class Y {}`
untyped __cpp__('{0} == {1}', x, y);

It is not going to work because the c++ compiler would not know that it should convert y, which could be in any promotion state, to const Y& or Y& for choosing the operator overload.

I could manually insert a double cast via __cpp__ but it doesn't look nice, and the native type name of y may not really be known here.

// haxe
untyped __cpp__('{0} == static_cast<Y&>(static_cast<ValueReference<Y>>({1}))', x, y);

Hence what I was trying to ask is that we could possibly make it nicer with some sort of conversion API on the Haxe side. e.g.:

// haxe
untyped __cpp__('{0} == {1}', x, cpp.ValueType.getReference(y));

and the case is similar when the actual value type value is needed. Maybe cpp.ValueType.getValue(y) would generate static_cast<Y>(static_cast<ValueReference<Y>>(y))

Not very sure if all these make sense at all, please let me know!

@kevinresol
Copy link
Contributor

kevinresol commented Mar 28, 2025

I also have question regarding null. Consider the following example regarding optional function argument:

// Foo is a @:cpp.PointerType extern class
function f(?native:Foo) {
  trace("native is null", native == null);
}

f(); // false, but I think it should be true from a Haxe point of view?

The generated c++ is roughly:

void f(Boxed<Foo*> native){
	::hx::IsEq( native,Boxed<Foo*>( new Boxed_obj<Foo*> (null())) );
}

f(Boxed<Foo*>( new Boxed_obj<Foo*> (null())));

@Aidan63
Copy link
Contributor Author

Aidan63 commented Mar 29, 2025

I see what you mean now, in that situation I'd write a small wrapper C++ functions, something like

bool _hx_x_eq_y(X* lhs, Y* rhs) {
    return (*lhs) == (*rhs);
}

and extern it to haxe with something like

function x_eq_y(lhs : cpp.Star<X>, rhs : cpp.Star<Y>) : Bool;

I'm a bit hessitant to add escape hatch functions because it opens up untyped cpp and I want to reduce the amount of that I see as much as possible. But with the above solution it does require something like an abstract to make it nice to use from haxe and requires writing a fair bit of boiler plate code.

Speaking of abstracts, before starting all this one alternative I did consider was using core type abstracts instead of extern classes as that would make all those operator overload scenarios possible.

function main() {
    final x = new X();
    final y = new Y();

    trace(x == y);
}

@:coreType
@:notNull
@:cpp.ValueType({ type : 'X' })
abstract X {
    public function new() : Void;

    @:op(A == B)
    public function eqY(rhs:Y) : Bool;
}

@:coreType
@:notNull
@:cpp.ValueType({ type : 'Y' })
abstract Y {
    public function new() : Void;
}

The one main kicker with this is that there's no nice way to represent inheritance, so I opted against it in the end and went with traditional extern classes. I like this idea but I'm not sure what to do with it since a lack of inheritance support is a pretty big issue.

I also have question regarding null

I think that was a bit of an oversight. I guess null checks involving boxed pointers should check the null-ness of the GC container and the null-ness of the held pointer. I don't think there are any scenarios where you'd want to differentiate between the two.

@kevinresol
Copy link
Contributor

kevinresol commented Mar 31, 2025

It is unfortunate that the example was a little bit misleading, the main culprit is not the operator overload per se, but how the c++ compiler sees a type and select the correct construct (function call, operator, etc). So it will still happen for templated function such as:

template<typename T>
void foo(T& ref);

template<typename T>
void bar(T* ptr);

In that case there is basically no way to call foo with a ValueType or bar with a PointerType because the c++ will not recognize these marshal types as a plain reference or pointer.

For me the way to call bar with a PointerType value v for example:

// works
extern static inline function bar<T>(v:T):Void 
  untyped __cpp__('bar({0})', ( v: cpp.Star<T> ));

// does not work
@:native("bar") extern static function bar<T>(v:cpp.Star<T>):Void;

And for c++ references unless we have something like cpp.Ampersand that does the same job as cpp.Star, we are left with doing the cast in untyped cpp.

@Aidan63
Copy link
Contributor Author

Aidan63 commented Mar 31, 2025

This is possible as long as you introduce a partial template specialisation or something similar. This specialisation would just be an intermediate which is specialised to a ValueReference and calls the original templated function with a reference to the value type. Full sample below.

@:cpp.ValueType
@:semantics(reference)
extern class Foo {
    var i : Int;

    function new() : Void;

    static function bar<T>(v : T) : Void;
}

@:headerNamespaceCode('
struct Foo {
    int i;

    template<class T>
    static void bar(T& v) {
        printf("base template %i\\n", v.i);

        v.i = 10;
    }

    template<class T>
    static void bar(::cpp::marshal::ValueReference<T> v) {
        printf("partial specialisation\\n");

        bar(*v.ptr);
    }
};
')
class Main {
    static function main() {
        final foo = new Foo();
        foo.i = 7;

        Foo.bar(foo);

        trace(foo.i);
    }
}

We trace the value of foo.i back in haxe to prove that we've been mutating a reference to the initially passed in value type, not a copy. In the end you should see something like this.

image

@kevinresol
Copy link
Contributor

kevinresol commented Apr 13, 2025

I think the compiler should be aware of any ValueType that is created inline and immediately converted to a reference. Because in that case the reference will become invalid as the value itself is out of scope straight away.

Consider the following pseudo generated code:

foo( (ValueReference<T>) (ValueType<T>(args)) )

When foo gets the reference and use it in the body, the reference is already invalid as the actual value itself is already out of scope.

In this case I believe the compiler needs to make sure the actual value can survive the foo call so it should generate something like:

ValueType<T> tmp(args);
foo((ValueReference<T>)(tmp)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants