Mostrando postagens com marcador software. Mostrar todas as postagens
Mostrando postagens com marcador software. Mostrar todas as postagens

Quinta-feira, Maio 08, 2008

Projeto Orientado a Componentes, parte IV

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.

Terça-feira, Abril 29, 2008

Projeto Orientado a Componentes, parte III

Agora, uma pausa para reflexão. Esta dicotomia interface versus implementação é difícil de definir. Por baixo dos panos nós temos memória de onde nós lemos e para onde nós escrevemos. O que caracteriza uma interface e o que a distingue de uma implementação?

Essa pergunta só pode ser respondida em um determinado contexto. Um bom sistema escrito em assembler talvez tenha interfaces mais bem definidas que um mau sistema escrito em Java. Porém, está claro para a indústria que certos mecanismos de linguagem favorecem o estabelecimento de boas interfaces no sistema implementado.

Além disso, a noção de interface surge diante de um problema de substituição. É interface aquilo que, mantendo-se estável, permite a substituição daquilo que é implementação. Porém, o que é este permite? Este permite, quando caracterizado, por conseguinte caracteriza o que é interface.

Suponha um sistema escrito em C++ por uma equipe que considera irrisório o tempo de recompilação do código-fonte. Neste contexto, podemos seguramente aceitar que a seguinte substituição mantém estável uma interface.

antes


class Foo {
public:

Foo ();

void
mutate_stuff ();

int
observe_stuff () const;

private:
Bar* m_bar;
};

depois

class Foo {
public:

Foo ();

void
mutate_stuff ();

int
observe_stuff () const;

private:
int m_cache;
shared_ptr<bar> m_bar;
};

Após a alteração acima e uma recompilação o sistema continua a funcionar normalmente. [1] Dizendo de forma mais extensa: neste contexto, a substituição realizada não altera propriedades observáveis externamente da classe, de modo que do ponto de vista de um observador, a classe mantém estável sua interface. (Apenas membros privados foram alterados.)

Agora vamos alterar as nossas premissas. O tempo de recompilação desse sistema é enorme e o ciclo de testes não pode esperar, de modo que a estrutura de classes do sistema foi particionada e esta classe mora dentro de um objeto compartilhado, uma biblioteca dinâmica.

A equipe realiza a substituição, recompila o objeto compartilhado e o entrega a uma equipe de testes.
E os testes falham miseravelmente.

Isto acontece porque, neste novo contexto, há mais propriedades observáveis a considerar, que devem se manter estáveis -- que fazem parte da interface da classe.
Neste caso, o layout de um objeto da classe Foo é uma propriedade observável; um programa compilado criando objetos da definição antes criará objetos com um layout diferente daquele esperado pelo objeto compartilhado que espera objetos dada a definição depois.
A definição depois tem um int no offset onde a definição antes tinha um ponteiro, ela ocupa mais memória etc.

Podemos dizer que o primeiro caso é o caso de um projeto orientado a objetos, onde a alteração de elementos privados da classe não altera a interface; e podemos dizer que o segundo caso é o caso de um projeto orientado a componentes, onde a alteração do layout na memória de uma classe altera sua interface.

Como no segundo caso nós estamos violando a interface do componente, nós não podemos substituir o componente de antes pelo componente de depois impunemente. Dito de trás para a frente, pelo fato de não podermos substituir o componente de antes pelo componente de depois, por definição estamos violando sua interface.

Imagine então o que acontece em um sistema spaghetti, onde todas as classes mantém referência a todas as outras classes; uma violação de interface em um "componente" perturbará implacavelmente todos os outros "componentes".

Quando projetamos um sistema pensando em componentes, ou quando desejamos aplicar a idéia forte de interface para melhorar o nosso projeto diminuindo o acoplamento entre as coisas que são distintas, é útil usar das ferramentas à disposição para dar forma ao que é uma interface.

Linguagens que se propõe a facilitar o desenvolvimento de componentes, como Java e C#, possuem como parte integrante do seu vocabulário e mecanismo nativo uma interface em contraste com as classes. Normalmente algo do tipo:

interface IFoo {

void
mutate_stuff ();

int
observe_stuff () const;

};

Em C++ não existe uma construção análoga mas é possível emulá-la integralmente usando classes, da seguinte maneira:

class IFoo {

virtual
void
mutate_stuff () = 0;

virtual
int
observe_stuff () const = 0;

};

(O leitor astuto observará que esta construção-interface não possui atributos.)

O propósito de usar construções como estas é evidenciar a natureza destes elementos como interfaces -- aquilo que se deseja manter estável -- e diminuir a chance disto que é interface ser inadvertidamente alterado -- causando o desastre. A construção-interface é a melhor amiga do projetista de componentes, permitindo a representação de um conceito de projeto diretamente na linguagem do código-fonte, restringindo as possibilidades de violação durante o processo de implementação, ou ao menos tornando a (necessidade de) violação claramente evidente.

Infelizmente para nós nem tudo que existe na fronteira de um componente pode ser uma interface, já que é preciso comunicar coisas de um componente para outro. E quem pode viver comunicando apenas doubles e bools? Nós queremos comunicar objetos.

Além disso, as construções-interface não são garantia de estabilidade de interfaces, já que há mais sobre o que é publicamente observável em um objeto que sua estrutura: há o seu comportamento.

[1] Estamos, naturalmente, assumindo que o programador não é louco e que a alteração tem algum sentido, exatamente como aparenta ter.

Segunda-feira, Abril 28, 2008

Projeto Orientado a Componentes, parte II

Quando dizemos que componentes são por definição intercambiáveis estamos continuando uma longa tradição de bons projetistas de bons sistemas, cujo conjunto de características sempre integrará, por mais sofisticada a metodologia e/ou a nomenclatura, a aplicação da separação entre interface e implementação. [1]

Se a interface continua a mesma, podemos substituir a implementação sem perturbar o comportamento do sistema. Obviamente que o comportamento do sistema deve mudar de alguma forma, senão trocar a porcaria do componente não teria propósito. Esperamos, portanto, que erros sejam corrigidos (de modo que o sistema de fato funcione como deveria), ou que o desempenho aumente (de modo que o sistema funcione antes que eu caia no sono) ou sei lá.

Aplicávamos esta distinção já na era do projeto estruturado; e no projeto essencial; e no projeto orientado a objetos; e agora no projeto orientado a componentes e aplicaremos no projeto orientado a serviços quando finalmente este troço alcançar a plebe.

Em um sistema orientado a objetos, nós temos tipicamente dois elementos em evidência como detalhe de implementação: a estrutura de um objeto, e as instruções levadas a cabo por seus métodos. Nós queremos liberdade para alterar a estrutura de um objeto e queremos liberdade para alterar a instruções executadas por seus métodos. (Há mais por debaixo do pano, como sempre.)

Em um sistema orientado a componentes, um terceiro elemento surge ofuscando, como se fosse, os outros dois; o componente propriamente dito. Entendemos que um componente possui uma interface e uma implementação; sendo a implementação de um componente composta por inúmeras classes. Além disso, um componente é um artefato componente (heh) de um sistema implantado; é parte da definição de componente que este seja intercambiável por um outro, que implemente a mesma interface, em um sistema implantado -- em contraste com um tipo de substituição que exige recompilação.

Esta última restrição, quando posta no contexto dos sistemas operacionais reais, com seus linkers e loaders reais, nos trás à crux do problema de projeto orientado a componentes: a implementação de uma classe integrante de um componente absolutamente não pode depender de um detalhe de implementação de uma classe integrante de outro componente.

Observe que esta restrição não é exatamente uma novidade, já que bons sistemas isolarão módulos através de interfaces para aumentar a coesão e diminuir o acoplamento; porém, quando componentes entram na jogada, a metodologia exige uma restrição mais forte.

(De fato, esta restrição metodológica se torna uma impossibilidade tecnológica assim que os componentes são levados ao próximo estágio, passam a não mais morar no mesmo espaço de memória virtual, e começam a ser chamados "distribuídos" ou "serviços".) [2]

É por esta razão que os líderes do Projeto Spaghetti falharão em "componentizar" seu sistema; mesmo que, à primeira vista, seja possível seccionar sua estrutura de classes em artefatos binários distintos.

[1] Nós, filhos da cultura européia, nem escrevendo programas nos livramos da dicotomia corpo versus espírito. Nem imagino o que seria uma metodologia de desenvolvimento de sistemas desenvolvida por monges taoístas chineses.

[2] E portanto, domar a restrição metodológica significa poder conviver com a impossibilidade tecnológica e eventualmente programar os sistemas do futuro.

Projeto Orientado a Componentes, parte I

Ano passado fiz um curso de extensão em Projeto Orientado a Componentes com UML; na época, eu estava progredindo na minha capacidade de construir modelos mentais de projetos de software orientados a objeto e já havia lido bastante sobre web services e arquiteturas orientada a serviços.

O mais interessante sobre os Componentes é a maneira como eles dão um nó na sofisticação aparente dos sistemas e obrigam o projetista a voltar aos básicos. Digo isso porque estes sistemas são aparentemente sofisticados; na prática, eles apresentam baixa coesão e alto acoplamento, significando que elementos deste sistema se relacionam diretamente com praticamente todos os outros de maneira ad hoc, fenômeno que, em sua forma mais concreta, denomina-se código spaghetti.

É possível construir um sistema orientado a objetos onde todos os objetos conhecem ponteiros para todos os outros, ou praticamente todos os outros, usando truques safados como embrulhar todo mundo em shared_ptr e tornando todas as classes enable_shared_from_this e olerê olará. Isto, é claro, se você tem um mínimo de conhecimento sobre C++ contemporâneo -- e se você está de fato escrevendo seu sistema em C++! -- e se você se importa com a memória. O sistema acima é "orientado a objetos". Muitos deles, embolados entre si.

Quando você se propõe a fazer sistemas orientados a componentes, porém, o buraco se alarga e se torna mais profundo. Os componentes existem com o propósito de serem intercambiáveis. Você não tem componentes se você não pode tirar um deles e colocar outro no lugar impunemente.

Frequentemente, os responsáveis por sistemas spaghetti ou aberrações similares, diante dos problemas insuportáveis causados inevitavelmente pela baixa coesão e pelo alto acoplamento, tentam "componentizar" seus sistemas com o intuito de obter os benefícios maravilhosos oferecidos pelos livros.

Vamos, então, pegar pedaços do sistema e enfiar dentro de DLLs!

Certamente isto não funciona.

Domingo, Abril 06, 2008

Projeto: Eclipse + CppUnit

Estou prestes a apresentar para o ccppbrasil.org um projeto de integração da CppUnit com o Eclipse TPTP.

Será uma oportunidade interessante de aprender a desenvolver sobre a plataforma OSGi e uma maneira de estimular a comunidade a aplicar as técnicas de testes de unidade.

A arquitetura do TPTP Test é assim: para um determinado "tipo de teste" registra-se um adaptador de implantação, um adaptador de ambiente e um adaptador de execução, bem como um runner para este "tipo de teste" no agente remoto.

A tarefa será implementar esses adaptadores para programas executáveis nativos e bibliotecas compartilhadas, resolvendo LD_LIBRARY_PATH e assuntos relacionados, depois implementar uma biblioteca de suporte em C para que programas joguem na saída padrão o histórico de execução da forma correta e por fim implementar com um wrapper Java um runner para suites de teste CppUnit na forma de plug-in.

Na verdade as tarefas ainda estão se abrindo porque não consegui dominar o problema de projeto completamente, o que me aborreceu um pouco, já que vou passar a semana ocupado com outros assuntos.

Quarta-feira, Abril 02, 2008

Encontro de Nerds em São Paulo

Ora, aqui estamos nós!
O Encontro foi muito bom, evoluindo o formato do anterior.
Os nerds são inteligentes e conhecedoras das coisas, e bebem bastante cerveja e são engraçados demais.
Nosso trabalho continua no site ccppbrasil.org.

Sábado, Março 29, 2008

Quarto Encontro CCPP Brasil

Cheguei há pouco na rodoviária de São Paulo.

Entrei em uma lan house pra anotar os endereços de que eu precisava. ¬¬

Cheguei cedo demais aqui; o ônibus fez a viagem em cinco horas e meia.
A turma só vai acordar depois das 07h, provavelmente às 08h.

Vou passar o tempo comendo e fumando.

Desta vez vou ficar hospedado no Formule One Jardins.
A estação de metrô da vez é Trianon-Masp.
Pra facilitar a minha vida comecei este mapa com marcadores para as estações do metrô.

Segunda-feira, Março 24, 2008

OpenMP

Reavaliar as hipóteses vigentes é sempre interessante quando você já possui alguma experiência; um retorno à "infância" do aprendizado, talvez, a uma época em que a sua sensação de segurança era mínima.

Acrescentar uma anotação tão singela quanto:

#pragma omp parallel
causou um desastre tremendo em um loop pequenininho dentro de pixman-mmx.c nesse fim de semana, experimentando com o André um novo caminho de otimização.

Se o seu compilador implementa OpenMP a diretiva acima transforma o bloco do loop seguinte de modo a despachar iterações individuais a um time de threads de trabalho.

Quando a hipótese de que uma iteração será seguida de outra some dependências entre acessos a dados surgem: enquanto uma iteração lê um endereço outra iteração escreve nesse enredeço, boom. Experimentamos muitos SIGSEGV com este loop. Eventualmente a implementação foi transformada de modo a eliminar essas dependências.

É interessante que se você não tem em mente essa questão não se dará ao trabalho e seus loops não terão iterações independentes. Porém, mesmo sem OpenMP, o GCC 4.3 produziu uma melhoria impressionante na execução deste código; certamente porque seus otimizadores aproveitaram novas oportunidades oferecidas por iterações livres de side effects observáveis.

Sábado, Março 15, 2008

ccppbrasil.org

yo! _o/

Domingo, Janeiro 13, 2008

Palestra em Sampa

Próximo sábado, dia 19, darei uma palestra em Sampa com o seguinte título:

"C++0x - Novas características de suporte a projetos de bibliotecas genéricas"

Estou um pouco excitado e um pouco nervoso com essa oportunidade.

Participarei do terceiro encontro do Grupo de Usuários C/C++.
A programação do evento está aqui.

Quinta-feira, Maio 31, 2007

Fedora 7 nasceu

Fedora é o sistema operacional livre que eu uso em casa.
Eu recomendo a todos e normalmente estou disposto a dar suporte voluntário (caveat emptor) para resolver problemas.

O ambiente de trabalho do Fedora é o GNOME e ele é muito bonito.

A versão 7 do sistema foi lançada hoje.
Vou instalar nesse fim de semana e dar o meu parecer.