Aidan Lee

New interop features with Hxcpp and Haxe 4.3

Haxe 4.3 is out and with it comes interop improvements both big and small for the c++ target, this article will cover all the interop changes in this new release as well as practical examples.

Generic Extern Classes

Prior to 4.3 externing templated classes required writing a fair bit of boiler plate code as generic extern classes caused the compiler to generate invalid C++. Say, for example, you wanted to extern std::vector<int>, that would require you to do the following.

@:structAccess
@:include('vector')
@:native('std::vector<int>')
extern class StdVectorInt {
    function push_back(v : Int) : Void;

    function data() : cpp.RawPointer<Int>;
}

Now, what if you wanted an extern of another std::vector type? You can’t use generic arguments with extern classes, so you have two choices. Either copy and paste that extern you wrote and change the types, or you could introduce an intermediate generic extern. The issue with generic extern classes was always code generation, but we can use them for code re-use and sub class them for the specialisations of the templated type.

private extern class StdVector<T> {
    function push_back(v : T) : Void;

    function data() : cpp.RawPointer<T>;
}

@:structAccess
@:include('vector')
@:native('std::vector<int>')
extern class StdVectorInt extends StdVector<Int> { }

@:structAccess
@:include('vector')
@:native('std::vector<double>')
extern class StdVectorFloat extends StdVector<Float> { }

Not brilliant as it still requires you to sub class the extern for every template specialisation you want to interop with, but it works. In haxe 4.3 all of this is no longer required! Generic extern classes can be used to represent templated types in C++ with no fuss.

@:structAccess
@:include('vector')
@:native('std::vector')
extern class StdVector<T> {
    function new();

	function push_back(_v : T) : Void;
}

function main() {
	final ivec = new StdVector();
	ivec.push_back(1);

	final fvec = new StdVector();
	fvec.push_back(1.4685);
}

Now generates the following C++.

void Main_Fields__obj::main(){
            	HX_STACKFRAME(&_hx_pos_195033e4c87195a0_1_main)
HXLINE(   2)		std::vector<int> ivec = std::vector<int>();
HXLINE(   3)		ivec.push_back(1);
HXLINE(   5)		std::vector<Float> fvec = std::vector<Float>();
HXLINE(   6)		fvec.push_back(((Float)1.4685));
            	}

Lovely!

haxe.Int64 Code Generation

Up until now the haxe.Int64 type was implemented as a stack allocated C++ class containing two ints, one for the high 32 bits, and the other for low 32 bits. If you wanted to interop with 64bit values you had to use cpp.Int64, but historically this extern type wasn’t usable in most haxe containers. haxe.Int64s were also boxed when used in arrays or as map values, due to this if you tried to use something like Pointer.arrayElem on a Array<haxe.Int64> you would not be getting an address to a contiguous chunk of memory for the 64bit ints in the same way you would with Array<Int>. With 4.3 haxe.Int64 is now implemented as an abstract around cpp.Int64, so the stack based class with two ints is no more as the generated code uses native C++ 64 integers. On top of that the Array and Map implementations have been updated to have dedicated 64bit int storage capabilities. Your 64bit ints will no longer be boxed and for arrays will be stored in contiguous memory opening up easier interop possibilities.

final a = [ 7i64, 6442450941i64 ];
final p = cpp.Pointer.arrayElem(a, 0);

// `p` is now a pointer to contiguous 64bit int memory.

Array Access for Externs

ArrayAccess is a special interface extern classes can implement which allows you to use array access syntax on extern classes. While this type allows you to use array access syntax on the haxe side it did not generate array access in the C++, instead it assumes the extern class implementing this interface is a subclass of hx::Object and generates function calls based on that assumption. In short, you couldn’t use this interface to use array access syntax on the haxe side and have the generated C++ use array access syntax. Going back to std::vector, you had to create an abstract and use untyped to manually generate array access.

@:structAccess
@:include('vector')
@:native('std::vector<int>')
private extern class StdVectorIntImpl {
    function push_back(v : Int) : Void;

    function data() : cpp.RawPointer<Int>;
}

abstract StdVectorInt(StdVectorIntImpl) {
    @:arrayAccess function get(i : Int) : Int {
        return untyped __cpp__('{0}[{1}]', this, i);
    }

    @:arrayAccess function set(i : Int, v : Int) : Int {
        untyped __cpp__('{0}[{1}] = {2}', this, i, v);

        return v;
    }
}

A new bit of meta has been introduced to address this, if your extern implements ArrayAccess and you add the :nativeArrayAccess meta it will generate array access syntax in the C++.

@:structAccess
@:nativeArrayAccess
@:include('vector')
@:native('std::vector')
extern class StdVector<T> implements ArrayAccess<cpp.Reference<T>> {
    function new(size : Int);

    function push_back(_v : T) : Void;
}

function main() {
    final ivec = new StdVector(1);
    ivec[0] = 7;

    trace(ivec[0]);
}
void Main_Fields__obj::main(){
            	HX_STACKFRAME(&_hx_pos_195033e4c87195a0_11_main)
HXLINE(  12)		std::vector<int> ivec = std::vector<int>(1);
HXLINE(  13)		ivec[0] = 7;
HXLINE(  15)		::Sys_obj::println(ivec[0]);
            	}

Extern Enums

It can often be easier to write the hxcpp glue code in C++, creating haxe objects in C++ and then extern those wrapper functions in the haxe side, but returning data as a haxe enum from C++ usually requires several lays of conversion.

The following code is from a libuv extern library, it converts a libuv error code into an “anonymous” enum in C++ and returns that to the haxe side.

hx::EnumBase create(const String& name, const int index, const int fields)
{
    auto result = new (fields * sizeof(cpp::Variant)) hx::EnumBase_obj;

    result->_hx_setIdentity(name, index, fields);

    return result;
}

hx::EnumBase uv_err_to_enum(const int code)
{
    switch (code)
    {
        case UV_ENOENT:
            return create(HX_CSTRING("FileNotFound"), 0, 0);
        case UV_EEXIST:
            return create(HX_CSTRING("FileExists"), 1, 0);
        
        /** Other cases omitted **/

        default:
            return create(HX_CSTRING("CustomError"), 13, 1)->_hx_init(0, String::create(uv_err_name(code)));
    }
}

To access this data in the haxe side we need to use the cpp.EnumBase api. I call these enums “anonymous” because they are not strongly named (switch expressions with them don’t work in the normal haxe way), so to make them more ergonomic to use in haxe you can then convert them to a normal haxe enum.

enum IoErrorType {
	/** File or directory not found */
	FileNotFound;
	/** File or directory already exist */
	FileExists;

    /** Other cases omitted **/

	/** Any other error */
	CustomError(message:String);
}

abstract AsysError(cpp.EnumBase) {
    @:to public function toIoErrorType() {
        return switch this._hx_getIndex() {
            case 0:
                IoErrorType.FileNotFound;
            case 1:
                IoErrorType.FileExists;
            
            /** Other cases omitted **/

            case 13:
                IoErrorType.CustomError(this.getParamI(0));
            default:
                IoErrorType.CustomError('Unknown Error ${ this._hx_getIndex() }');
        }
    }
}

This obviously isn’t great as we have two of the same enum and if you’re using this quite a lot the allocations could add up. This code is also quite brittle, if you change an index value in the C++ side you need to remember to change it in the haxe side. There are ways you could work around this, e.g., externing an enum from the C++ side which defines the integer values, but you’ll still need to perform this conversion to make the enum nice to use in haxe.

Extern enums are designed to work around this, you can define a fully named haxe enum in C++ (essentially hand writing what the compiler would generate) and then externing it to haxe. The advantage of this is that these enums are usable from haxe the same as any haxe generated enum is and are a lot less brittle.

HX_DECLARE_CLASS0(IoErrorType)

class IoErrorType_obj : public hx::EnumBase_obj {
public:
    typedef IoErrorType_obj OBJ_;

    enum Type {
        TFileNotFound,
        TFileExists,
        /** Other enums omitted **/
    }

    IoErrorType_obj() = default;

    HX_DO_ENUM_RTTI;

    String GetEnumName() const { return HX_CSTRING("IoErrorType"); }
    String __ToString() const { return HX_CSTRING("IoErrorType.") + _hx_tag; }

    IoErrorType FileNotFound() {
        return hx::CreateEnum<IoErrorType_obj>(HX_CSTRING("FileNotFound"), Type::TFileNotFound, 0);
    }

    IoErrorType FileExists() {
        return hx::CreateEnum<IoErrorType_obj>(HX_CSTRING("FileExists"), Type::TFileExists, 0);
    }

    static bool __GetStatic(const String& inName, Dynamuc& outValue, hx::PropertyAccess access) {
        if (inName == HX_CSTRING("FileNotFound")) { outValue = IoErrorType_obj::FileNotFound_dyn(); return true; }
        if (inName == HX_CSTRING("FileExists")) { outValue = IoErrorType_obj::FileExists_dyn(); return true; }
        // etc, etc...

        return hx::EnumBase_obj::__GetStatic(_inName, _outValue, _propAccess);
    }

    hx::Val __Field(const String& inName, hx::PropertyAccess access) {
        if (inName == HX_CSTRING("FileNotFound")) { return IoErrorType_obj::FileNotFound_dyn(); }
        if (inName == HX_CSTRING("FileExists")) { return IoErrorType_obj::FileExists_dyn(); }
        // etc, etc...

        return hx::EnumBase_obj::__Field(_inName, _propAccess);
    }

    int __FindIndex(String inName) {
        if (inName == HX_CSTRING("FileNotFound")) { return TFileNotFound; }
        if (inName == HX_CSTRING("FileExists")) { return TFileExists; }
        // etc, etc...

        return hx::EnumBase_obj::__FindIndex(_inName);
    }

    int __FindArgCount(String inName) {
        if (inName == HX_CSTRING("FileNotFound")) { return 0; }
        if (inName == HX_CSTRING("FileExists")) { return 0; }
        // etc, etc...

        return hx::EnumBase_obj::__FindArgCount(_inName);
    }

    HX_DEFINE_CREATE_ENUM(IoErrorType_obj)

    STATIC_HX_DEFINE_DYNAMIC_FUNC0(IoErrorType_obj, FileNotFound, return)
    STATIC_HX_DEFINE_DYNAMIC_FUNC0(IoErrorType_obj, FileExists, return)

    hx::Class IoErrorType_obj::__mClass;
};
// If you follow hxcpp naming convensions you won't need to annotate the extern with any meta.

extern class IoErrorType {
    FileNotFound;
    FileExists;
    /** Other enums omitted **/
}

function main() {
    switch create() {
        case FileNotFound:
            trace('not found');
        case FileExists:
            trace('file exists');
        /** Other enums omitted **/
    }
}

function create() : IoErrorType {
    return untyped __cpp__('IoErrorType_obj::FileExists()');
}

Yes, you need to type a lot of code (you’re essentially doing the compilers job) and in reflection areas it could break if you forget to update a name (you could probably improve on the code I wrote above in that area), but it allows you to use the enum straight away after you pass it into haxe. Not an ideal solution and in many cases, it may be better to keep enums just to haxe code to avoid this entirely, but the option is now available to you, pick your poison!

Multiple Meta

Finally you can now use multiple hxcpp specific metadata on externs, previously only the first would apply and the rest silently ignored. This prevents you from needing to use the much more verbose header code meta to include multiple header files or using proxy inheritance objects to make sure the includes are properly imported.

@:headerInclude("header1.h")
@:headerInclude("header2.h", "header3.h")
@:cppInclude("header4.h", "header5.h")
class Main
{
    static function main()
    {
        SomeLib.someFunc();
    }
}

@:include("other_header1.h", "other_header2.h")
@:include("other_header3.h")
@:depend("Class1.h", "Class2.h")
@:depend("Class3.h")
extern class SomeLib
{
    @:native("someFunc") static function someFunc() : Void;
}