Uma ligeira digressão, para aproveitar a oportunidade que se apresenta; apresentei esta semana um brevíssimo seminário sobre interoperabilidade entre C++ e Java (com ênfase na ferramenta SWIG).
O problema a resolver, quando a necessidade de interoperabilidade entre C++ e a Java surge, é exatamente um problema de projeto de componente, por uma via indireta: se não é tão necessária a garantia de substituição de componentes, é inescapável a completa separação entre a implementação do "componente C++" e do "componente Java" devido ao completo isolamento entre seus espaços de memória.
Esta mesma situação ocorre em um sistema convencional de componentes em que cada componente se localiza em processos distintos, ou mesmo em sistemas distintos, se comunicando através de algum mecanismo inter-processos. O próximo passo na escala evolutiva do projeto de sistemas, o projeto orientado a serviços, lida explicitamente com esta situação, já que se assume como normal que serviços se localizam em sistemas (portanto processos) distintos.
Esse contexto, e as restrições que ele impõe, transforma a natureza da troca de informação; a memória de um processo não é mais um recurso compartilhado entre rotinas.
Considere a seguinte função em C, parte de uma interface de componente IFoo. (O componente concreto Foo implementa a interface IFoo.)
Foo*
foo_retrieve_from_persistence (char* foo_name);
Esta função recebe uma NTBS identificando unicamente um objeto Foo na persistência e retorna o endereço do objeto Foo na memória, trazendo o objeto para a memória uma primeira vez se necessário.
Suponha que seja desejável, através de um programa Java, usar objetos Foo obtidos através desta função.
Do componente Bar, uma operação obtém uma referência a uma implementação de IFoo e decide chamar a operação foo_retrieve_from_persistence, passando como argumento uma NTBS. Observe que uma NTBS é uma referência a um espaço contíguo de memória; simplesmente entregar esse endereço para o componente Foo causará um desastre quando este componente resolver acessar esse endereço em seu próprio espaço de memória, cujo significado é incerto. Do mesmo problema sofre o valor de retorno da função; este é o endereço de um objeto na memória do componente Foo, cujo significado no espaço da memória de Bar é incerto.
Essa situação exibe a impossibilidade de tratar a semântica de referência, na travessia do limiar entre componentes, da mesma maneira como é tratada em projetos mais simples. Como já dissemos, não é possível assumir a memória como recurso compartilhado entre operações em um projeto orientado a componentes. [1]
A solução canônica é copiar esses valores. Esta tarefa deve ser realizado por aquele elemento do sistema que existe no umbral entre componentes e é responsável por transportar informação de um lado para o outro. Este elemento deverá, então, copiar todo o segmento de memória endereçado por foo_name da memória do componente Bar para a memória do componente Foo. Esta atividade se denomina "data marshalling" em um certo vocabulário e regras particulares de "data marshalling" são chamadas "type maps" em um certo outro vocabulário.
Este é um exemplo simples de "type map" que transporta um objeto String do Java para um argumento de tipo const std::string& do C++.
// na prática, estes objetos são argumentos de uma função JNI.
extern JNIEnv* jenv;
extern jstring jargN;
// type map
const char *argN_pstr = (const char *)jenv->GetStringUTFChars(jargN, 0);
if (!argN_pstr) return 0;
std::string argN_str(argN_pstr);
A função GetStringUTFChars realiza a tarefa concreta de copiar os segmentos de memória do espaço de memória Java para o espaço de memória C++. Assim, a operação da interface de componente Java/JNI terá a seguinte forma:
public static native jlong foo_retrieve_from_persistence (String foo_name);
Essa solução, infelizmente, não resolverá o problema do valor de retorno da função. Foo não é um tipo primitivo da linguagem; não existe uma função na JNI para copiar objetos Foo. Mesmo que existisse, Foo é um objeto; o que nós queremos fazer com ele é chamar suas operações. De certa forma, nossa vontade se divide em duas: expor ao programa Java as operações da classe Foo e expor ao programa Java objetos Foo sobre o qual operar. Essas duas necessidades serão resolvidas com mecanismos diferentes.
Digamos que esta seja a classe Foo.
class Foo {
public:
Foo (char* name);
char*
ask_question (char* question);
char*
get_name () const;
};
A primeira vontade é realizável produzindo, para cada operação de Foo, uma operação na interface do componente IFoo.
Foo*
Foo_new (char* name);
void
Foo_delete (Foo* foo);
char*
Foo_ask_question (Foo* foo, char* question);
char*
Foo_get_name (const Foo* foo);
A segunda é entregando ao programa Java o endereço do objeto Foo necessário. Chegamos, aparentemente, a um impasse; já que nosso problema original era justamente como transportar o valor de retorno da função foo_retrieve_from_persistence, que é do tipo Foo*! Porém, tendo caminhado até aqui, o problema se torna ligeiramente diferente, e uma solução é possível. Agora que nós temos todas as operações de Foo que desejamos exportadas pela interface de componente IFoo, tudo o que resta é entregar ao componente cliente uma referência opaca a um objeto Foo. O componente cliente da interface nunca necessitará resolver (ou de-referenciar) esta referência; ele apenas mantém este valor para usá-lo como argumento de chamada a uma operação da interface IFoo. [2]
Endereços de memória do C++ podem ser guardados apropriadamente na memória do Java como um valor do tipo Long. Assim, o valor de retorno da operação Foo_create é resolvido pelo seguinte "type map":
// este é o objeto retornado pela chamada JNI
jlong jresult = 0;
// este é o objeto retornado pela função C++
Foo* result = NULL;
// type map
result = new Foo(arg1_str); // vide type map anterior
*(Foo **)&jresult = result;
// por fim...
return jresult;
e a operação na interface de componente Java/JNI terá a seguinte forma:
public static native jlong Foo_create (String name);
Assim, o programa Java chamador de Foo_create através desta interface receberá um valor Long que contém o endereço, na memória do C++, do objeto Foo recém-criado. A operação Foo_get_name através desta interface terá a seguinte forma:
public static native String Foo_get_name (jlong foo);
sendo que o valor de retorno da operação Foo_get_name do componente IFoo será "marshalled" pelo "type map" como já vimos anteriormente.
Havendo resolvido o problema da possibilidade de referenciar, de maneira opaca, um objeto Foo da memória do C++ na memória do Java, e o problema de chamar operações da classe Foo em C++, podemos então criar uma classe Foo em Java cujo único propósito é imitar a classe Foo em C++ por conveniência.
class Foo {
private Long cPtr;
// @Override
public void finalize () {
delete();
}
public void delete () {
IFooJNI.Foo_delete(cPtr);
}
public Foo (String name) {
cPtr = IFooJNI.Foo_new(name);
}
public String ask_question (String question) {
return IFooJNI.Foo_ask_question(cPtr, question);
}
public String get_name () {
return IFooJNI.Foo_get_name(cPtr);
}
};
Desta forma concretizamos uma forma ideal de interoperabilidade entre Java e C++ onde, para cara classe C++, há uma classe Java equivalente, responsável por esconder as chamadas à interface de componente JNI que, por sua vez, é responsável por realizar o transporte de informação através do umbral dos componentes.
Nestes exemplos ocultamos o fato de que, para a JNI, é necessário não somente um componente Java/JNI mas também um componente C++/JNI além do próprio componente que implementa a interface IFoo; também não lidamos com os casos em que registramos "objetos callback" criados no Java em sistemas C++; não lidamos com a vontade de transportar exceções disparadas em C++ de volta para o chamador Java; entre outras coisas de que não falamos.
[1] É claro que um projeto pode se utilizar da noção de componentes de uma forma restrita, para obter um conjunto restrito de benefícios, abandonando esta restrição; é possível, por exemplo, obter os benefícios de recompilação veloz e carga de código por demanda abandonando a possibilidade de distribuir os componentes de modo a mantê-los sempre no mesmo espaço de memória e permitir o uso convencional de ponteiros.
[2] Em projetos de sistemas com componentes é comum que esta idéia do "endereço opaco" seja generalizada para qualquer tipo de informação "opaca" que seja capaz de identificar univocamente um objeto de Foo no domínio do componente Foo; por exemplo, um "nome", ou um GUID, ou outra coisa qualquer.