Skip to content

HashLink native extension tutorial

Zeta edited this page Dec 14, 2021 · 8 revisions

I would like to share here my experience on how to write a C/C++ HashLink extension. My main sources of documentation were:

Why creating an HashLink extension?

I see at least 2 reasons:

  • Add a wrapper to an existing C/C++ library, so it can be used inside Haxe.

  • Performance: HashLink is fast, HashLink/C (HL bytecodes compiled to C and then compiled natively) is faster, but a native extension is even faster. So for specific cases where performance is critical (terrain generation in a game for example), it might be a good solution.

I wrote a little benchmarck here to compare performances. Here are the results:

Time (seconds) Relative time
Native 3.88 1.00
HashLink/C 5.91 1.52
HashLink 16.76 4.32
Native Debug 48.16 12.41

Create the Visual Studio project

I used Visual Studio on Windows to create my extension. First, create an empty project and change the following settings:

  • Set the 'Active solution platform' to x64 in the 'Configuration Manager'. That's important, because HashLink is a 64-bits application, thus it won't load a 32 bit extension.

  • Add the HashLink include directory in 'Additional Include Directories'. On my Windows, the environment variable $(HASHLINKPATH) is already defined and point to the HashLink directory. So just add $(HASHLINKPATH)\include for this setting.

  • In the general properties, set 'Configuration Type' to be 'Dynamic Library (.dll)'.

  • Not mandatory but useful, add a Post-Build command to copy the target dll to the location where is the compiled hl project, and to rename it with an hdll file extension. Here is for example the command I used: copy $(TargetDir)$(TargetFileName) ..\$(TargetName).hdll

HashLink C interface file

The C/C++ file has to start with this:

#define HL_NAME(n) <project_name>_##n

#include <hl.h>

Note: On older versions of hashlink (ie before https://github.com/HaxeFoundation/hashlink/commit/6405fe5055e3cd6c07e719c22b76df0bb01e66e8), when including a header that includes stdbool.h (or defines true, false and bool), include it before hl.h. Otherwise you might get confusing errors about mismatching function signatures (or other errors).

The macro on the first line must be defined before the inclusion of hl.h, because this header file relies on it.

The rest of the file contains all the function that will be exposed to Haxe. These functions look like this:

HL_PRIM <return_type> HL_NAME(<function_name>)(<arguments>);

Here is an example:

#define HL_NAME(n) simplex_##n

#include <hl.h>

HL_PRIM vbyte* HL_NAME(generate)(int width, int height, int seed)
{
    vbyte* buffer = hl_alloc_bytes(width * height);
    
    ...

    return buffer;
}

vbyte is a type defined in hl.h, and in this example, the function returns a memory buffer allocated by the function hl_alloc_bytes from the HashLink C Api (also defined in hl.h). On the Haxe side, the returned data is here an object of type hl.Bytes (and not haxe.Bytes).

The C API Documentation provides a detailed list of allocation functions which work in conjunction with the Haxe garbage collector.

Note that the snake case naming convention (lowercase with an underscore between words) has to be used for function names, else these functions won't be recognized when the extension will be loaded by HashLink.

At the end of the c file, the exposed functions must have their signature being declared. Here is an example:

DEFINE_PRIM(_BYTES, generate, _I32 _I32 _I32);

This macro has 3 parts:

  • The returned type.

  • The function name.

  • The types of the arguments (separated by a space, not a comma).

The header file hl.h defines all values that can be used to define a function signature:

Name C/C++ equivalent
_VOID
_I8 signed char
_I16 signed short
_I32 signed integer
_I64 signed long long
_F32 float
_F64 double
_BOOL bool
_BYTES vbyte*
_DYN
_FUN(t, args)
_OBJ(fields)
_ARR
_TYPE
_REF(t)
_ABSTRACT(name)
_NULL(t)

Note that the Haxe Float is a 64 bits floating number and thus corresponds to the C/C++ double. The Haxe Single corresponds to the C/C++ float.

If a function has no argument, then the special macro _NO_ARG has to be used for the third part.

Haxe declaration

A C/C++ extension needs to have a dedicated Haxe file to declare all functions created in the extension. It looks like this:

package ext;

@:hlNative("simplex")
class SimplexGenerator
{
    public static function generate(width : Int, height : Int, seed : Int) : hl.Bytes {return null;}
}

The metadata @:hlNative("simplex") indicates that the functions are defined in a native extension. "simplex" is here the name of extension set in the previous c interface file, but also this has to be the name of the compiled hdll file. However, the package and the class name can be named differently.

The class contains all functions defined in the extension, and a default function block (here {return null;}) needs to be defined, even if it won't be used.

Unlike the C/C++ extension, here the camel case naming convention (uppercase at the beginning of each word, except for the first one) has to be used when naming functions.

Passing parameters to the native function

The type list shown above gives the basic types that can be passed to a native function. But more complex ones can be passed as well.

String

hl.h already contains the definition of vstring which is a type for an HashLink string. Here is an example of how to use it:

HL_PRIM bool HL_NAME(begin)(vstring* name) { ... }

vstring is defined as:

typedef struct {
	hl_type *t;
	uchar *bytes;
	int length;
} vstring;

Note that bytes is a pointer on 16 bits unicode characters, so it needs to be converted if the extension uses UTF8.

Function

An Hashlink function can be passed as a parameter. The function is passed as a closure like this example:

HL_PRIM vdynamic* HL_NAME(initialize)(vclosure* render_fn) { ... }

The function signature has to use _FUN for this parameter and its own parameter has to be also present in the signature like this example:

DEFINE_PRIM(_DYN, initialize, _FUN(_VOID, _DYN));

Here the native function initialize returns a dynamic type and takes a function as parameter which doesn't return anything (_VOID) and takes only one parameter, a dynamic (_DYN).

If the function parameter isn't used immediately and stored in the extension to be called later, it needs to increment its reference counting, else the garbage collector might remove it. To do so, this function needs to be called:

hl_add_root(&render_fn);

hl_remove_root() decrements its reference counting.

To call the HashLink function:

vdynamic* args[1];
args[0] = param;
hl_dyn_call(render_fn, args, 1);

Here, in this example, param is a dynamic variable created with hl_alloc_dynobj() and filled with data (see below).

Structure

The function parameters can be packed into a structure (it can be useful for example if too many parameters need to be passed to the extension).

On the C side, the function looks like this:

HL_PRIM vbyte* HL_NAME(generate)(vdynamic* build_settings)
{
    Settings settings;

    // get values of the keys "width" and "height" from the Haxe structure (C/C++ type: int, Haxe type: Int)
    settings.width = hl_dyn_geti(build_settings, hl_hash_utf8("width"), &hlt_i32);
    settings.height = hl_dyn_geti(build_settings, hl_hash_utf8("height"), &hlt_i32);

    // get the value of key "ratio" (C/C++ type: double, Haxe Type: Float)
    settings.ratio = hl_dyn_getd(build_settings, hl_hash_utf8("ratio"));
    
    ...

The function hl_hash_utf8 creates an integer hashed value of a string.

To return a structure to Haxe, a dynamic variable needs to be created and filled with keys/values:

HL_PRIM vdynamic* HL_NAME(build)(vdynamic* build_settings)
{
    ...

    vdynamic* obj = (vdynamic*)hl_alloc_dynobj();       // the cast is safe here as it's the way used in the HashLink standard library
    hl_dyn_setp(obj, hl_hash_utf8("map"), &hlt_bytes, map);
    hl_dyn_seti(obj, hl_hash_utf8("level"), &hlt_i32, level);

    return obj;
}

The C API Documentation provides a list of all hl_dyn_getXXX and hl_dyn_setXXX functions.

On the Haxe side, the extension declaration becomes:

@:hlNative("isgen")
class SimplexGenerator
{
    public static function generate(build_settings : Dynamic) : Dynamic {return null;}
}

However, the parameter and the returned value have a generic Dynamic type, and it can be enhanced with a type declaration for both:

// constraint the generic type Dynamic to the type T
abstract ExtDynamic<T>(Dynamic) from T to T {}

// parameter structure
typedef BuildSettings = {
    width : Int,
    height : Int,
    ratio : Float
}

// returned structure
typedef Simplex = {
    map : hl.Bytes,
    level : Int
}

@:hlNative("isgen")
class SimplexGenerator
{
    public static function build(build_settings : ExtDynamic<BuildSettings>) : ExtDynamic<Simplex> {return null;}
}

Here the function parameter and the returned value are still Dynamic, but constrained to a defined structure.

HashLink/C compilation

When the target is HashLink/C, the bytecodes are converted to plain C code. From this c code, a Visual Studio project can be created to compile it, or it can compiled directly on the command line.

The previous dll compilation has produced a dll file, and a lib file as well. This lib file needs to be linked to include the proper interface to the dll.

Extension debugging

It can be debugged as a regular dll file. For this purpose, set the following Debugging settings:

  • 'Command' has to be $(HASHLINKPATH)\hl.exe

  • Add the name of the compiled hl file in 'Command Arguments'

  • Set the path of the directory where is the hl compiled file in 'Working Directory'