Question

I want to create a Dynamic library (cross-platform) in D, so I did some Googling. After some time I found this page. I am absolutely stunned by how much complexities there are in writing, compiling and even linking to a DLL. Isn't there a uniform way of creating a shared library like you would in C? (just leave out the main function and pass some flags to the linker)

Was it helpful?

Solution

Well, I decided to spend some time today messing with this and I kinda sorta have something that works, at least if the main program is also written in D (on Linux, I think it will work from C too on Windows. The reason is I didn't link to phobos in the .so in the D one, so it relies upon the exe for those symbols. I think, tbh I don't know exactly what is going on here, maybe it would work better if I used the shared phobos lib too)

Anyway, first, let's throw some code down.

This is testdll.d and it builds our dll

module testdll;
import std.stdio;
extern(C)
export void lol() {
    import core.stdc.stdio;
    printf("hello from C\n");

    writeln("hello!");
}


version(Windows)
extern(Windows) bool DllMain(void* hInstance, uint ulReason, void*) {
import std.c.windows.windows;
import core.sys.windows.dll;
    switch (ulReason)
{
    default: assert(0);
case DLL_PROCESS_ATTACH:
    dll_process_attach( hInstance, true );
    break;

case DLL_PROCESS_DETACH:
    dll_process_detach( hInstance, true );
    break;

case DLL_THREAD_ATTACH:
    dll_thread_attach( true, true );
    break;

case DLL_THREAD_DETACH:
    dll_thread_detach( true, true );
    break;
  }
  return true;
}

You'll notice most that code is the WinMain which just calls druntime functions. I think that main should be available at least as a mixin, or maybe even fully automatic, since it is pure boilerplate.

And the client code:

import core.runtime;

alias extern(C) void function() functype;

version(Posix) {
    extern(C) void* dlsym(void*, const char*);
    extern(C) void* dlopen(const char*, int);
    extern(C) char* dlerror();

    pragma(lib, "dl");
} else version(Windows) {
    extern(Windows) void* LoadLibraryA(const char* filename);
    extern(Windows) void* GetProcAddress(void*, const char*);
}

void main() {
    version(Posix) {
            auto thing = dlopen("./testdll.so", 2);
            if(thing is null) {
                    import std.conv;
                    import std.stdio;
                    writeln(to!string(dlerror()));
                    return;
            }
            auto proc = cast(functype) dlsym(thing, "lol");
    } else version(Windows) {
            auto thing = LoadLibraryA("testdll.dll");
            assert(thing !is null);
            auto proc = cast(functype) GetProcAddress(thing, "lol");
    }
    assert(proc !is null);
    //import std.stdio; writeln("calling proc");
    proc();
}

This has different code for Windows and Linux, though it is pretty similar. The druntime stuff is supposed to start taking care of this soon as we mentioned in the comments.

The compile commands aren't too bad but a little weird. Linux first:

dmd -fPIC -shared testdll.d -defaultlib= # builds the dll

PIC and shared tell it to build the .so. I did the blank defaultlib because without it, loading the dll at runtime failed with "symbol already defined" errors.

Building the client is straightforward:

dmd testdllc.d

Note that there's the pragma(lib) in the file that links with the -ldl option automatically. Run it and get some hello! BTW be sure both are in the same directory since this loads ./ in the loader.

Now, let's build on Windows.

dmd -oftestdll.dll -shared testdll.d testdll.def

Tell it to output our dll, use -shared so it knows, and then the other thing is the def file, like described here http://dlang.org/dll.html/dllmain

These are the contents of that file:

LIBRARY testdll

EXETYPE NT
CODE SHARED EXECUTE
DATA WRITE

EXPORTS
        lol

If you don't use the .def file, the dll will build successfully, but the procedure won't be found because it isn't exported. (I think the export keyword in D should be able to do this automatically, bypassing hte .def file, and I believe there's a discussion on doing this, but right now it is required as far as I know.)

And the client is similarly easy:

dmd testdllc.d

Run it and get some hellos, if all goes well.

Now, why did I do the functype alias in the client? Easier than doing the other casting and such, and it makes it nicely extern(C).

Why is the lol function extern(C) in the first place? Just so it has an easier name to use in GetProcAddress/dlsym. Could have also pragma(mangle) or did .mangleof with an import thing. All kinds of options there, fairly straightforward, I just wanted to keep it simple to make the test easier to focus on. "lol" is a simpler name than "_D7testdll3lolFZv" or whatever the mangled name would be.... (OMG I mangled it correctly by hand! Sometimes I think I write too much D lol) and yeah that works too it is just harder to do by eyeball. Note: on Windows, the .def file might have to leave off the leading underscore if you do it that way.

anyway, yeah, this made a working dll/so for me and a program to load and use it successfully. Not as pretty as it could/should be, but it works. For me at least.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top