www.digitalmars.com
Last update Sun Aug 20 17:04:33 2006

Escrevendo DLLs Win32 em D

DLLs (Dynamic Link Libraries - Bibliotecas de Ligação Dinâmica) são uma das fundações da programação de sistemas para Windows. A linguagem de programação D possibilita a criação de vários tipos de DLL diferentes.

Este guia mostrará como criar DLLs de vários tipos com D.

DLLs com uma interface C

Uma DLL apresentando uma interface C pode se conectar a qualquer outro código em uma linguagem que suporte chamar funções C em uma DLL. DLLs podem ser criadas em D aproximadamente da mesma forma que em C. Um DllMain() é requerido, parecendo com:
import std.c.windows.windows;
HINSTANCE g_hInst;

extern (C)
{
void gc_init();
void gc_term();
void _minit();
void _moduleCtor();
void _moduleUnitTests();
}

extern (Windows)
BOOL DllMain(HINSTANCE hInstance, ULONG ulReason, LPVOID pvReserved)
{
switch (ulReason)
{
case DLL_PROCESS_ATTACH:
gc_init(); // inicializa o GC
_minit(); // inicializa lista de módulos
_moduleCtor(); // executa construtores do módulo
_moduleUnitTests(); // executa testes de unidade do módulo
break;

case DLL_PROCESS_DETACH:
gc_term(); // derruba o GC
break;

case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
// Múltiplos threads ainda não são suportados
return false;
}
g_hInst=hInstance;
return true;
}
Notas: Linke com um .def (Arquivo de Definição de Módulo) com as linhas:
LIBRARY MYDLL
DESCRIPTION 'Minha DLL escrita em D'

EXETYPE NT
CODE PRELOAD DISCARDABLE
DATA PRELOAD SINGLE

EXPORTS
DllGetClassObject @2
DllCanUnloadNow @3
DllRegisterServer @4
DllUnregisterServer @5
As funções na lista EXPORTS são para ilustração. Substitua ela com as funções exportadas de MYDLL. Alternativamente, use implib. Aqui está um exemplo de uma simples DLL com uma função print() que imprime uma string:

mydll2.d:

module mydll;
export void dllprint() { printf("hello dll world\n"); }

mydll.def:

LIBRARY "mydll.dll"
EXETYPE NT
SUBSYSTEM WINDOWS
CODE SHARED EXECUTE
DATA WRITE
Ponha o código acima que contém DllMain() em um arquivo dll.d. Compile e linke a dll com o seguinte comando:
C:>dmd -ofmydll.dll mydll2.d dll.d mydll.def
C:>implib/system mydll.lib mydll.dll
C:>
que criará mydll.dll e mydll.lib. Agorea para um programa, test.d, que usará a dll:

test.d:

import mydll;

int main()
{
mydll.dllprint();
return 0;
}
Crie um clone de mydll2.d que não tenha os corpos das funções:

mydll.d:

export void dllprint();
Compile e linke com o comando:
C:>dmd test.d mydll.lib
C:>
e execute:
C:>test
hello dll world
C:>

Alocação de Memória

DLLs D usam gerenciamento de memória com coleta de lixo. A questão é o que acontece quando ponteiros para dados alocados passam dos limites da DLL? Se a DLL apresenta uma interface C, alguém diria que a razão para isso é conectar com código escrito em outras linguagens. Essas outras linguagens não saberão sobre o gerenciamento de memória de D. Sendo assim, a interface C terá que defender os chamadores da DLL de precisarem saber qualquer coisa sobre isso.

Há muitas aproxmações para resolver esse problema:

Programação COM

Muitas interfaces da API do Windows estão nos termos de objetos COM (Common Object Model, também chamados objetos OLE ou ActiveX). Um objeto COM é um objeto onde o primeiro campo é um ponteiro para uma vtbl[], e as 3 primeiras entradas nessa vtbl[] são para QueryInterface(), AddRef(), e Release().

Objetos COM são análogos a interfaces D. Qualquer objeto COM pode ser expressado como uma interface D, e todo objeto D com uma interface X pode ser exposto como um objeto COM X. Isso significa que D é compatível com objetos COM implementados em outras linguagens.

Enquando não é estritamente necessário, a bibliotecas Phobos fornece um Objeto útil como uma super classe para todos os objetos COM D, chamado ComObject. ComObject fornece uma implementação padrão para QueryInterface(), AddRef(), e Release().

Objetos COM Windows usam as convenções de chamada do Windows, que não são o padrão para D, então funções COM precisam ter o atributo extern (Windows). Então, para escrever um objeto COM:

import std.c.windows.com;

class MyCOMobject : ComObject
{
extern (Windows):
...
}
O código amostra inclui um exemplo de cliente COM e um servidor DLL.

Código D chamando código D em DLLs

Tendo DLLs em D sendo capazes de se comunicarem com outras como se fossem linkadas estaticamente é, logicamente, muito desejável assim código entre aplicações pode ser compartilhado, e diferentes DLLs podem ser desenvolvidads independentemente.

A dificuldade é o que fazer com a coleta de lixo (gc). Cada EXE e DLL terão suas próprias instancias de gc. Enquando essas gc podem coexisir sem pisar na outra, é redundante e ineficiênte ter múltiplas gc's sendo executadas. A idéia explorada aqui é escolher uma gc e ter as DLLs redirecionando suas gc's para essa. A única gc usada  aqui será a do arquivo EXE, porém também é possível fazer uma DLL separada só para a  gc.

O exemplo mostrará como carregar estaticamente uma DLL, e carregar/descarregar ela dinamicamente.

Começando com o código da DLL, mydll.d:

/*
* MyDll demonstração de como escrever DLLs em D.
*/

import std.c.stdio;
import std.c.stdlib;
import std.string;
import std.c.windows.windows;
import std.gc;

HINSTANCE g_hInst;

extern (C)
{
void _minit();
void _moduleCtor();
void _moduleDtor();
void _moduleUnitTests();
}

extern (Windows)
BOOL DllMain(HINSTANCE hInstance, ULONG ulReason, LPVOID pvReserved)
{
switch (ulReason)
{
case DLL_PROCESS_ATTACH:
printf("DLL_PROCESS_ATTACH\n");
break;

case DLL_PROCESS_DETACH:
printf("DLL_PROCESS_DETACH\n");
std.c.stdio._fcloseallp = null; // assim stdio não será fechado
break;

case DLL_THREAD_ATTACH:
printf("DLL_THREAD_ATTACH\n");
return false;

case DLL_THREAD_DETACH:
printf("DLL_THREAD_DETACH\n");
return false;
}
g_hInst = hInstance;
return true;
}

export void MyDLL_Initialize(void* gc)
{
printf("MyDLL_Initialize()\n");
std.gc.setGCHandle(gc);
_minit();
_moduleCtor();
// _moduleUnitTests();
}

export void MyDLL_Terminate()
{
printf("MyDLL_Terminate()\n");
_moduleDtor(); // executa destrutores do módulo
std.gc.endGCHandle();
}

static this()
{
printf("static this for mydll\n");
}

static ~this()
{
printf("static ~this for mydll\n");
}

/* --------------------------------------------------------- */
class MyClass
{
char[] concat(char[] a, char[] b)
{
return a ~ " " ~ b;
}

void free(char[] s)
{
delete s;
}
}

export MyClass getMyClass()
{
return new MyClass();
}
DllMain
Este é o ponto de entrada principal para qualquer DLL em D. É chamado pelo código de inicialização C (para DMC++, o fonte é \dm\src\win32\dllstart.c). Os printf's foram colocados para traçar como ela é chamada. Note que o código de inicialização e terminação visto no exemplo DllMain anterior não está aqui. Isso porque a inicialização dependerá de quam está carregando a DLL, e como ela é carregada (estatica ou dinamicamente). Não há muito a fazer aqui. Singularidade é o ajuste de std.d.stdio._fcloseallp para nulo. Se isso não for feito, o C estimular e fechar todos os bufferes de E/S padrão (como stdout, stderr, etc.) derrubando outras saídas. Ajustando para nulo adia a responsabilidade para o chamador da DLL.

MyDLL_Initialize
Assim, em vez de termos nossa própria rotina de inicialização da DLL tão exatamente quando for chamada, pode ser controlada. Deve ser chamada após o chamador se inicializar, a biblioteca Phobos, e os construtors do módulo (normalmente seria quando main() entra). Essa função recebe um argumento, um controle para a gc do chamador. Veremos como esse controle é obtido depois. Em vez de gc_init() sendo chamado para inicializar a gc da DLL, std.gc.setGCHandle() é chamado e passado o controle para qual gc usar. Esse passo informa a gc do chamador quais áreas de dados da DLL varrer. Depois segue a chamada para _minit() para inicializar as tabelas do módulo, e _moduleCtor() para executar os construtores do módulo. _moduleUnitTests() é opcional e executa os testes de unidade da DLL. A função é exportada, assim se torna visível fora da DLL.

MyDLL_Terminate
Correspondentemente, essa função termina a DLL, e é chamada antes de descarregá-la. Ela têm duas funções; chamar os destrutores do módulo da DLL via _moduleDtor() e informar que a DLL não mais usará a gc do chamador via std.gc.endGCHandle(). Esse último passo é crítico, como a DLL será desmapeada da memória, e a gc continuará a varrer suas áreas de dados, isso causará falta de segmentos.

static this, static ~this
São exemplos de construtores e destrutores estáticos do módulo, aqui com uma impressão em cada cara verificar que eles são executados e quando.

MyClass
É um exemplo de uma classe que pode ser exportada e usada pelo chamador de uma DLL. A função membro concat aloca alguma memória com gc, e a função free libera memória de gc.

getMyClass
Uma manufatura exportada que aloca uma instância de MyClass e retorna uma referência para ela.

Para construir a DLL mydll.dll:
  1. dmd -c mydll -g
    Compila mydll.d em mydll.obj. -g liga a geração de informações de depuração.

  2. dmd mydll.obj \dmd\lib\gcstub.obj mydll.def -g -L/map
    Linka mydll.obj em uma DLL chamada mydll.dll. gcstub.obj não é necessário, mas previne o volume de código com gc de ser linkado, já que não será usado. Salva cerca de 12Kb. mydll.def é o Arquivo de Definição de Módulo, e tem o conteúdo:
    LIBRARY MYDLL
    DESCRIPTION 'MyDll demonstration DLL'
    EXETYPE NT
    CODE PRELOAD DISCARDABLE
    DATA PRELOAD SINGLE
    -g liga a geração de informação de depuração, e -L/map gera um arquivo de mapeamento mydll.map.

  3. implib /noi /system mydll.lib mydll.dll
    Cria uma biblioteca de importação mydll.lib bem vinda para linkar com uma aplicação que carregará estaticamente mydll.dll.

Aqui está test.d, uma aplicação amostra que usa mydll.dll. Há duas versões, uma se liga estaticamente a DLL, e a outra carrega-a dinamicamente.
import std.stdio;
import std.gc;

import mydll;

//version=DYNAMIC_LOAD;

version (DYNAMIC_LOAD)
{
import std.c.windows.windows;

alias void function(void*) MyDLL_Initialize_fp;
alias void function() MyDLL_Terminate_fp;
alias MyClass function() getMyClass_fp;

int main()
{ HMODULE h;
FARPROC fp;
MyDLL_Initialize_fp mydll_initialize;
MyDLL_Terminate_fp mydll_terminate;

getMyClass_fp getMyClass;
MyClass c;

printf("Start Dynamic Link...\n");

h = LoadLibraryA("mydll.dll");
if (h == null)
{ printf("error loading mydll.dll\n");
return 1;
}

fp = GetProcAddress(h, "D5mydll16MyDLL_InitializeFPvZv");
if (fp == null)
{ printf("error loading symbol MyDLL_Initialize()\n");
return 1;
}

mydll_initialize = cast(MyDLL_Initialize_fp) fp;
(*mydll_initialize)(std.gc.getGCHandle());

fp = GetProcAddress(h, "D5mydll10getMyClassFZC5mydll7MyClass");
if (fp == null)
{ printf("error loading symbol getMyClass()\n");
return 1;
}

getMyClass = cast(getMyClass_fp) fp;
c = (*getMyClass)();
foo(c);

fp = GetProcAddress(h, "D5mydll15MyDLL_TerminateFZv");
if (fp == null)
{ printf("error loading symbol MyDLL_Terminate()\n");
return 1;
}

mydll_terminate = cast(MyDLL_Terminate_fp) fp;
(*mydll_terminate)();

if (FreeLibrary(h) == FALSE)
{ printf("error freeing mydll.dll\n");
return 1;
}

printf("End...\n");
return 0;
}
}
else
{ // static link the DLL

int main()
{
printf("Start Static Link...\n");
MyDLL_Initialize(std.gc.getGCHandle());
foo(getMyClass());
MyDLL_Terminate();
printf("End...\n");
return 0;
}
}

void foo(MyClass c)
{
char[] s;

s = c.concat("Hello", "world!");
writefln(s);
c.free(s);
delete c;
}
Vamos começar pela versão linkada estaticamente, que é mais simples. Ela é compilada e linkada com o comando:
C:>dmd test mydll.lib -g
Note como ela é linkada com mydll.lib, a biblioteca de importação para mydll.dll. O código é simples, ele inicializa mydll.lib com uma chamada para MyDLL_Initialize(), passando o controle para a gc de test.exe. Então, podemos usar a DLL e chamar suas funções como se fossem parte de test.exe. Em foo(), memória gc é alocada e liberada por test.exe e mydll.dll. Quando terminarmos de usar a DLL, ela é terminada com MyDLL_Terminate().

A execução disso parecerá com:

C:>test
DLL_PROCESS_ATTACH
Start Static Link...
MyDLL_Initialize()
static this for mydll
Hello world!
MyDLL_Terminate()
static ~this for mydll
End...
C:>
A versão com linkagem dinâmica é um pouco mais difícil de configurar. Compile e linke-a com o comando:
C:>dmd test -version=DYNAMIC_LOAD -g
A biblioteca de importação mydll.lib não é necessária. A DLL é carregada com uma chamada para LoadLibraryA(), e cada função exportada é recuperada via chamada para GetProcAddress(). Um jeito simples para decorar o nome a passar para GetProcAddress() é copiar e colar do arquivo mydll.map gerado sob o cabeçalho Export. Uma ver que isso foi feito, podemos usar as funções membro das classes da DLL como se fossem parte de test.exe. Quando terminar, libere a DLL com FreeLibrary().

A execução disso parecerá com:

C:>test
Start Dynamic Link...
DLL_PROCESS_ATTACH
MyDLL_Initialize()
static this for mydll
Hello world!
MyDLL_Terminate()
static ~this for mydll
DLL_PROCESS_DETACH
End...
C:>