Quinta-feira, Fevereiro 24, 2011

.NET C++ Wrapper, Mixed Managed C++, C#, Mono, OGRE... e SWIG!

Como parte do meu projeto do mestrado, eu tenho que escolher uma game engine para o desenvolvimento de um protótipo de um framework adaptável de um jogo. Depois de feita algumas análises, eu optei por trabalhar com a OGRE3D, uma excelente engine de renderização 3D, escrita em C++, e completar o resto das funcionalidades com outras bibliotecas.

O projeto é escrito em C# 4, visando o MS .NET Runtime e o Projeto Mono como máquina virtual, e como a biblioteca OGRE3D é escrita em C++, as funcionalidades não podem ser acessadas diretamente, sendo necessária a utilização de um wrapper para a interoperabilidade. Assim sendo, existem duas escolhas a disposição para dar acesso a tais bibliotecas:
  • MOGRE - Um wrapper feito em Mixed Managed C++, com excelente desempenho, e;
  • OGRE.NET - Outro wrapper, que realiza as chamadas através de P/INVOKE, sendo gerado através do SWIG.
Acontece que a MOGRE, apesar de ser uma ótima biblioteca, somente provê suporte para o sistema operacional Windows, justamente por ter sido escrita utilizando código misto. Assemblies em Mixed Managed C++ são basicamente uma mistura de código nativo em C/C++ (x86 ou x86-64) com código gerenciável (managed code ou CIL). Como o código nativo é específico da plataforma que foi compilado, o Mono não consegue executá-lo, pois chamadas a funções do Windows estão presentes somente no Windows (bom isso é uma das limitações).

Uma alternativa seria utilizar compilação condicional, assim como é feito em C++ para portar uma biblioteca para diversas plataformas (a OGRE faz isso!). Ainda, assim, seria necessário que o GCC emitisse CIL e que gerasse um assembly em Mixed Code que fosse suportado pelo Mono, o que não é possível no momento.

O projeto requer que este o seja portável entre várias plataformas, afinal existem várias universidades, cada uma com sua escolha de sistema operacional (Windows, Linux, MacOSX). Sendo assim, a MOGRE passa a ser uma escolha limitada. Desse forma, resta analisar a biblioteca OGRE.NET. 

Por sua vez, a OGRE.NET é um projeto que aparenta estar morto. A última biblioteca criada somente dá suporte a versão 1.4 da OGRE, que já se encontra na versão 1.7.2. Sendo assim, não é interessante utilizá-la.

Se ambas propostas não são aceitáveis ao projeto, só resta criar uma alternativa, criar uma nova biblioteca que faça o trabalho. Tal biblioteca deve ser baseada em P/INVOKE que nada mais é do que um modo de se acessar bibliotecas dinâmicas (.dll, .so, .dylib etc) presentes no sistema operacional, através de chamadas a métodos estáticos externos.

Criar tal wrapper para uma biblioteca como a OGRE é um trabalho extremamente penoso (e como é!!), pois toda a funcionalidade dessa deve ser exposta como chamadas de função em C, imagine então portar todos os métodos de cada classe da OGRE; um grande trabalho, com certeza! Além disso, existem algumas situações onde é necessário manter o polimorfismo entre as classes e não somente criar Proxies (criação de Directors, como definida pelo SWIG).

Uma alternativa para se realizar tal tarefa é utilizar o SWIG, um gerador que pode ler o código em C++ (headers no caso) e gerar os wrappers necessários em C++ e C# automaticamente. E realmente, pra boa parte do trabalho, o SWIG consegue prover bons resultados, gerando um bom código.

Como em tudo existe um porém, o SWIG visa portar a biblioteca para a linguagem de destino (nesse caso, C#), mas não se preocupa muito em como tal biblioteca irá parecer perante os olhos de um programador acostumado a plataforma de destino. Surgem algumas construções estranhas na linguagem final; tipos com nomes estranhos, exposição de funcionalidades que parecem com as providas pelo framework mas que não o são, sendo incompatíveis algumas vezes, como em Listas, Dicionários etc.

Adicionalmente, o SWIG aparenta não suportar a criação de tipos de valor (structs) em C#, tudo é gerado como uma classe (uma das maiores limitações, em minha opinião). Isso provoca um efeito negativo alto no desempenho final da aplicação, que já está prejudicada pela utilização de P/INVOKE. Afinal, cada chamada a um método externo requer de 10 a 30 instruções somente para tal invocação; um constante desperdício de tempo de processamento para cada chamada externa.

Dessa forma, a construção de código manualmente, apesar de ser extremamente trabalhosa, é a melhor alternativa no momento para realizar tal tarefa. Uma possível alternativa, seria gerar com o SWIG o máximo possível de funcionalidades e deixar somente as mais problemáticas sendo realizadas manualmente. Mas o código do SWIG nem sempre é compatível com tal situação, ainda assim é um teste que irei realizar.

0 comentários: