AVISO

AVISO: ESTE É MEU ANTIGO BLOG, QUE NÃO É MAIS ESCRITO DESDE 2011. O CONTEÚDO AQUI EXPRESSO PODE NÃO REPRESENTAR MEUS PENSAMENTOS E OPINIÕES DE IDADE ADULTA. PARA CONTEÚDOS NOVOS E RELEVANTES ACESSE BLOG.BRUNO.TODAY




quinta-feira, 16 de outubro de 2008

Carregando bibliotecas dinâmicas em C++ com a dlopen API

Palavras-chave: plugins, C++, dlopen, bibliotecas dinâmicas

Bom, este é meu primeiro post com conteúdo mesmo. Quero demonstrar uma solução para uma dificuldade comum que alguns programadores C++ podem ter em determinadas situações. As bibliotecas dinâmicas são extremamente úteis, principalmente em sistemas de plugins. Desta maneira podemos desenvolver aplicativos mais dinâmicos, e deixar que outros usuários criem novas funcionalidades para eles. E não é só isso, tenha a necessidade e verás!!

A primeira vez que tive a necessidade de fazer isso, um tempo atrás, achei poucas referências sobre o assunto, nenhuma em Português. Destaque para o "C++ dlopen mini-HOWTO". Me surpreendi em saber que ninguém havia feito uma tradução dele, ou escrito algo sobre isso em língua portuguesa até hoje, e por isso escrevo este post.



Ele explica como carregar bibliotecas dinâmicas em ambientes Unix(especialmente GNU, que como o nome diz não é Unix :-). Quando precisamos carregar na linguagem C uma biblioteca, especialmente nos ambientes citados acima, é muito fácil. Simplesmente utilizamos a dlopen() API da maneira que ela foi projetada para funcionar. Esta API é composta das seguintes funções básicas:
  • void *dlopen(const char *path, int flags) - carrega a biblioteca dinâmica e retorna um handler
  • void *dlsym(void *handler, const char *symbol) - Retorna um ponteiro para o símbolo desejado(uma função ou uma variável/estrutura da biblioteca)
  • int dlclose(void *handler) - descarrega a biblioteca dinâmica e retorna um status
  • char *dlerror(void) - retorna descrição do ultimo erro ocorrido na API
Até aí, aparentemente nada impede o uso delas em C++, apesar de ficar a pergunta: "e se eu quero carregar uma classe, por exemplo?". Mas indo mais a fundo, ou para programadores C++ mais experiêntes, o problema torna-se visível: "name-mangling".

Não vou falar muito neste post sobre este cara de nome esquisito. Mas basicamente, os linkers adicionam ao arquivo uma função em C com um nome. Este nome é o nome da própria função. Mas agora imaginem a situação: C++ trabalha com sobrecarga. E se eu tiver duas funções com o mesmo nome e parâmetros diferentes?? Para isso, o compilador C++ modifica o nome das funções antes de gerar o arquivo-objeto. Esta modificação segue um padrão de notação de nomes, que depende da implementação do compilador(apesar de existirem 2 padrões bastante seguidos). "Isso" recebe o nome esquisito de 'name-mangling'.

Quando chamamos a função dlsym(), ela não consegue "adivinhar" que o name-mangling do C++ 'hackeou' o nome da função, pois ele foi escrito em C e "nem sabia" que o tal de "C++" existia. E agora?? Bom, o C++ possui um recurso para manter compatibilidade de nomes com o C. Este recurso é a declaração 'extern "C"'. Veja um exemplo de como utilizá-la:


extern "C" int funcao(int parametro); //declaração unica a não ser "mangleada"
extern "C" {
/* várias declarações a não serem "mangleadas" */
int funcao2(void);
int funcao3(int pqp);
int var1;
}
Agora, temos alguns cuidados quando nossos programas forem códigos que possam ser compilados com um compilador C. O problema é que o compilador C não sabe que existe o 'extern "C"', pois a linguagem C não possui esse recurso(por razões obvias). E agora jacaré? Meu código será exclusivamente C++, mesmo que seja uma porção estruturada do código que fiz para poder reaproveitar em um programa escrito em C??

Calma, veja o exemplo de como o pré-processador de macros nos salvará:


#ifdef __cplusplus

extern "C" {
/* bla bla bla */
}
#endif


Isso normalmente é útil para bibliotecas :-) Mas e aí, qual é a minha falando disso se estou falando de bibliotecas dinâmicas em C++?? Vamos lá! Funções C++ realmente terão perdas nessa história, pois perderemos a sobrecarga. Mas classes e métodos(o que realmente mais interessa no C++) ganham maior dinamismo. Como assim??

É simples. Podemos ter diferentes classes carregadas dinamicamente, sem o programa sequer "saber que ela existe" em tempo de compilação. A única restrição é que ela precisa herdar uma classe virtual(aka abstrata) que nós já declaramos(por motivos óbvios).

Ok, vamos colocar a mão na massa então?? Ok. É hora de definir requisitos do nosso aplicativo de teste:
-Exibir 'Hello, Brunildz is a beautiful man, world' na tela;
-Perguntar ao usuário o nome do arquivo de plugin a ser carregado;
-Carregar o plugin;
-Fazer as chamadas aos métodos GetFoo() e GetBar() das classes dos plugins;
-Retornar ao prompt solicitando o plugin novamente;
-Quando o valor de resposta do usuário for '0', sair;

Requisitos dos plugins Foo e Bar;
-Exibir a mensagem 'Foo'(para o plugin Foo) ou 'Bar'(para o plugin Bar) quando for carregado;
-Implementar classe imprimindo o nome de cada método chamado quando eles forem chamados;
-Classe deve seguir a interface do aplicativo;

Para isso, "precisamos" criar uma classe abstrata 'BrunildzPluginInterface', além de uma definição de tipo para um ponteiro para função retornando um objeto de nossa classe, em um arquivo que poderiamos chamar de BrunildzPluginInterface.hpp:

#ifndef __BRUNILDZPLUGININTERFACE_HPP

#define __BRUNILDZPLUGININTERFACE_HPP

class BrunildzPluginInterface {
public:
BrunildzPluginInterface() { }
virtual ~BrunildzPluginInterface() { };
virtual void GetFoo() = 0;
virtual void GetBar() = 0;
};


extern "C" {
typedef BrunildzPluginInterface *BPI_load_t();
typedef void BPI_unload_t(BrunildzPluginInterface *);
}

#endif//__BRUNILDZPLUGININTERFACE_HPP

Agora podemos criar o nosso primeiro plugin, chamado Foo. Para isso, precisamos de uma classe, uma função carregadora, e uma função descarregadora, que podemos criar no arquivo Foo.cpp(eu sei, ficaria mais limpo e reaproveitável dividir em Foo.h e Foo.cpp, mas é só um exemplo, aqui no blog fica mais bonito assim):

#include <BrunildzPluginInterface.hpp>

#include <iostream>

using namespace std;

class Foo : public BrunildzPluginInterface {
public:
Foo() {
   cout << "Plugin Foo: Foo();" << endl;
   }
~Foo() {
   cout << "Plugin Foo: ~Foo();" << endl;
   }
void GetFoo() {
   cout << "Plugin Foo: GetFoo();" << endl;
   }
void GetBar() {
   cout << "Plugin Foo: GetBar();" << endl;
   }
};


extern "C" {
BrunildzPluginInterface *load_plugin(void) {
   return new Foo;
}

void unload_plugin(BrunildzPluginInterface *bpi) {
   delete bpi;
}
}

Agora faremos o nosso plugin Bar, seguindo exatamente a mesma lógica:


#include <BrunildzPluginInterface.hpp>

#include <iostream>

using namespace std;

class Bar : public BrunildzPluginInterface {
public:
Bar() {
   cout << "Plugin Bar: Bar();" << endl;
   }
~Bar() {
   cout << "Plugin Bar: ~Bar();" << endl;
   }
void GetFoo() {
   cout << "Plugin Bar: GetFoo();" << endl;
   }
void GetBar() {
   cout << "Plugin Bar: GetBar();" << endl;
   }
};


extern "C" {
BrunildzPluginInterface *load_plugin(void) {
      return new Bar;
   }
void unload_plugin(BrunildzPluginInterface *bpi) {
      delete bpi;
   }
}
Ok, nossos plugins estão criados. E agora?? Basta fazermos o "trabalho sujo", ou seja, nossa aplicação. Vamos chamá-la de ldplugin.cpp:

#include <iostream>

#include <string>
#include <dlfcn.h>
#include <BrunildzPluginInterface.hpp>

using namespace std;


int main() {
string plugin, swp;
void *plugin_h = NULL;
BPI_load_t *loader = NULL;
BPI_unload_t *unloader = NULL;
BrunildzPluginInterface *instancia = NULL;

cout << "Hello, Brunildz is a beautiful man, world" << endl;

while (1) {
   cout << "Digite o nome do plugin(0 para sair): ";
   cin >> plugin;

   if (plugin == "0") break;

   swp = "./";
   swp += plugin;
   plugin = swp;

      plugin += ".so";
      plugin_h = dlopen(plugin.c_str(), RTLD_LAZY);
   if (!plugin_h) {
      cout << "Erro: " << dlerror() << endl;
      return 1;
      }
   loader = reinterpret_cast<BPI_load_t*>(dlsym(plugin_h,
                             "load_plugin"));
   unloader = reinterpret_cast<BPI_unload_t*>(dlsym(plugin_h,
                             "unload_plugin"));
   if (!loader || !unloader) {
      cout << "Erro: " << dlerror() << endl;
      return 1;
      }
   instancia = loader();
   instancia->GetFoo();
   instancia->GetBar();
   unloader(instancia);
   cout << "Proximo simbolo..." << endl;
   }
return 0;
}
Agora, para compilar é só executar os seguintes comandos(supondo que todos os arquivos acima estejam no diretório atual):

$ g++ -I. -fPIC -shared -g -o Bar.so Bar.cpp
$ g++ -I. -fPIC -shared -g -o Foo.so Foo.cpp
$ g++ -I. -ldl -rdynamic -o ldplugin ldplugin.cpp



Palavras-chave de busca que mais trouxeram a este artigo:
-dlsym class loading
-implementar biblioteca dinamica em C++
-biblioteca cout (!!!)
---
Classificação do conteúdo: SÉRIO
Sobre Bruno Moreira Guedes:
Curriculum Vitae
Site Pessoal

4 comentários:

Augusto Pinz disse...

Ótimo Blog!!
Espero mais postagens deste tipo, estou conseguindo bons resultados com as dicas.
abraço

brunoRomano disse...

Amigo, muito bom seus post's continue assim.


Se você souber mais sobre os __attribute__ e puder fazer um artigo sobre isso seria muito interressante.

Como vc havia dito para sugerir algum assunto estou sugerindo.

txithihausen disse...

Cara, muito bom o sesu artigo. Mostra bem passo-a-passo em como criar e invocar bibliotecas dinâmicas. Parabéns! :)
Sugiro a quem estiver interessado, dar uma lida no seguinte texto http://www.nuclex.org/articles/building-a-better-plugin-architecture . Ele dá umas dicas de como criar uma arquitetura baseada em plugins bem robusta!

Gabriel Becker disse...

Excelente artigo, Parabéns!

Sobre Bruno Moreira Guedes