Aldacron's Guide to Derelictification



Introduction

Because Derelict is open source, the project is open to contributions from users. You are also free to modify your copy of the source under the terms of the BSD-style license agreement you'll find at the top of every Derelict source module. Still, even though contributions are welcome, not all are accepted.

Bugfixes and enhancements to existing code or documentation will almost always be folded in. New packages, however, often will not be. If we were to accept any and all packages submitted, Derelict would quickly become bloated, unwieldy, and difficult to maintain. New packages will most definitely be added over time, but they must meet some loose criteria, as explained on the front page.

Whether you are making a package for inclusion in the trunk, distributing it separately, or using it privately, there are a few guidelines that should be followed when creating the package.

The Bindings

At a very minimum, the bindings you create must be designed to allow manual loading of a shared library. If the library to which you are binding cannot be compiled in shared library form, then the binding cannot be Derelictified. Creating a shared library binding involves several steps, which this document attempts to clarify.

typedefs, constants and #defined values

C typedefs can translate to either alias or typedef statements. Derelict modules should use the alias form. Constants should be declared in anonymous enums. #defined values, depending on the intention, should either be declared as aliases or anonymous enums.
Derelict packages originally declared constants as publicly available constant values. In order to help reduce the size of the resultant binaries, a process was begun to convert all such values to anonymous enums. Currently, the conversion is not complete so you will see examples of both usages in the packages. New packages should all use the enum form. Example:
enum
{
	CONSTANT_ONE    = 10,
	CONSTANT_TWO 	= 22,
	CONSTANT_THREE	= 25,
}

Where to put these declarations depends entirely upon the size of the library being bound. Using DerelictSDL as an example, you can look at the source to see that all types are declared in context-specific modules along with function declarations and converted macros. Because of the number of headers in SDL, this approach makes sense. DerelictAL (and most of the other Derelict packages), on the other hand, declares all types in a special types module, while functions are declared separately. Generally, this is the approach you should take when the size of the binding is not so large to make this approach difficult to maintain.

Macros

The D Programming Language has no means of implementing macros, so C macros must be converted to a format D understands. This means the macros should be converted to D functions. The functions should be declared and implemented publicly at the global module scope. They should not be declared as part of a class or struct definition. And while it may be tempting to implement some macros as templates or mixins, don't. The goal is to stay as close to the C version as possible.

Function Declarations

All Derelict packages must provide function pointer declarations. When a shared library is manually loaded, the function pointers will be initialized to point to the proper memory locations in the shared library's address space. The declarations of the function pointers should be typedefed and should have the following syntax:

return_type function(param_list) pfFunctionName

In the above, pf is prepended to the C function name without changing the case of the name. For example, the function name glColor would become pfglColor. More examples:

typedef void function(int) pfMyFunction;            // typedefed pointer to C function named MyFunction
typedef int function() pfMyOtherFunction;           // typedefed pointer to C function named MyOtherFunction
typedef int function(float, char*, void*) pfyetAnotherMyFunction; // typedefed pointer to C function named yetAnotherMyFunction.

Following the function declarations should be global variables of the type pfFunctionName (for each function), and named FunctionName. Expanding the above example:

typedef void function(int) pfMyFunction;
typedef int function() pfMyOtherFunction;
typedef int function(float, char*, void*) pfyetAnotherMyFunction;
pfMyFunction            MyFunction;                 // pointer to C function named MyFunction
pfMyOtherFunction       MyOtherFunction;            // pointer to C function named MyOtherFunction
pfyetAnotherMyFunction  yetAnotherMyFunction;       // pointer to C function named yetAnotherMyFunction.

Finally, you should keep related function delcarations and their associated variable declarations grouped according to purpose. That is, rather than making a long list of function declarations followed by a long list of variable declarations, break them up. This really helps to ease maintenance. One or two of the Derelict packages currently do not do this, but those will be restructured in the future. Expanding on the above examples:

typedef void function(int) pfMyFunction;
typedef int function() prMyOtherFunction;
typedef int function(float, char*, void*) pfyetAnotherMyFunction;
pfMyFunction            MyFunction;
pfMyOtherFunction       MyOtherFunction;
pfyetAnotherMyFunction  yetAnotherMyFunction;

typedef float function() pfFooFunction;
typedef double function(int) pfAnotherFooFunction;
pfFooFunction           FooFunction;
pfAnotherFooFunction    AnotherFooFunction;

Loading the Shared Library

The last step is to implement the actual loading of the shared library. This involves binding the function pointers to the function names and aliasing a GenericLoader template. Both require templates that are defined in derelict.util.loader, so you must import that module at the top of your module:
private import derelict.util.loader;

The binding of a function is done via the bindFunc template. First, you must implement a function that will handle all of the loading. This function should be private to your module. The function should accept a SharedLib as the only argument.

private void loadFoo(SharedLib lib)
{

}

If you are unfamiliar with the mechanics of D templates, the call syntax of bindFunc may be quite strange to you. In this case, there are two sets of parantheses. In the first set of parentheses, you need to pass the function pointer to which you are binding the function. The second set of parentheses is where, in this particluar case, you provide the arguments to an opCall implementation internal to bindFunc. Here, you should pass the name of the function as a string and the SharedLib reference passed to your load function. It's easier to understand seeing it in action than reading a description of it. The following example expands on the previous examples:

private import derelict.util.loader;

typedef void function(int) pfMyFunction;
typedef int function() prMyOtherFunction;
typedef int function(float, char*, void*) pfyetAnotherMyFunction;
pfMyFunction            MyFunction;
pfMyOtherFunction       MyOtherFunction;
pfyetAnotherMyFunction  yetAnotherMyFunction;

typedef float function() pfFooFunction;
typedef double function(int) pfAnotherFooFunction;
pfFooFunction           FooFunction;
pfAnotherFooFunction    AnotherFooFunction;

private void loadFoo(SharedLib lib)
{
	bindFunc(MyFunction)("MyFunction", lib);
	bindFunc(MyOtherFunction)("MyOtherFunction", lib);
	bindFunc(yetAnotherFunction)("yetAnotherFunction", lib);
	bindFunc(FooFunction)("FooFunction", lib);
	bindFunc(AnotherFooFunction)("AnotherFooFunction", lib);
}

The last thing to do is to instantiate an instance of the GenericLoader struct and configure it to load your library by calling its setup method:

GenericLoader.setup(char[] windowsNames, char[] linuxName, char[] macNames, void function(SharedLib) loadFunc)

Each of the name parameters are either a single library name or a comma-separated list of library names, that the loader will attempt to find on the user's system when the load method is called with no arguments. When multiple names are supplied, they will be loaded in the order given. As an example, here is the implementation for DerelictAL:

GenericLoader DerelictAL;
static this()
{
	DerelictAL.setup(
		"OpenAL32.dll",
		"libal.so, libAL.so, libopenal.so, libopenal.so.0",
		"",
		&loadAL
	);
}

So continuing the foo example, assuming there is a foo.dll on Windows and a libFoo.so on Linux:

GenericLoader DerelictFoo
static this()
{
	DerelictFoo.setup(	"foo.dll",      // the Windows shared library name(s)
	                    "libFoo.so",    // the Linux shared library name(s)
	                    "",            	// the Mac shared library name(s)
	                    loadFoo        	// the load function where your bindFunc calls are
	);
}

The use of multiple shared library names is important on Linux. There are many possible names for the same shared library, so it is best to provide as many of them as you know of to the loader. The loader will attempt to load the shared library using each of the names given until it is successful and will not throw an exception until the last name is attempted and fails.

Use the Source, Luke

If you find this documentation confusing (I do, and I wrote it), then the source is your best recourse. It's fairly self-explanatory. I recommend that you use DerelictAL and DerelictSDL as examples, for consistency.