2007/11/23

Desenvolvimento Série 60 para quem desenvolve em Linux - parte 2

"O senhor é um fanfarrão!" -- Capitão Nascimento falando ao arquiteto do Symbian

A API do Symbian é um produto legítimo dos anos 90: baseia-se fortemente em design patterns C++. No caso de uma aplicação estilo 'Hello World' gerada pelo SDK, temos as seguintes classes (para uma aplicação chamada EPx):

CEPxApplication: representa a aplicação como um todo

CEPxAppUi: controla a UI, representa o "controller" do modelo MVC. Na prática, o grosso da aplicação vai ser codificada nesta classe, ou pelo menos é a partir desta classe que o resto do seu código é invocado.

CEPxAppView: representa uma "visão" da UI, pode haver várias visões. É obviamente o "view" do MVC.

CEPxDocument: representa o "Model" do modelo MVC.

O ponto de entrada da aplicação é a função E32Main() no código-fonte EPx.cpp. Neste mesmo arquivo há outra função chamada NewApplication(). A aplicação é carregada da seguinte forma:

* E32Main() chama EikStart::RunApplication(NewApplication).
* tendo recebido o endereço via RunApplication, o framework chama NewApplication().
* NewApplication() instancia um objeto CEpxApplication.
* O framework invoca CEpxApplication::CreateDocumentL(), que instancia um objeto CEPxDocument.
* O framework invoca CEPxDocument::CreateAppUiL() que cria um objeto CEPxAppUI.
* O framework invoca CEPxAppUi::ConstructL() que contém e instancia um objeto CEpxAppView.

Enfim, barroco ao extremo.

O próximo choque cultural do desenvolvedor não-Symbian é o "tratamento de exceções". Como as exceçòes e templates do C++ costumavam ter problemas de implementação em todos os compiladores, o Symbian criou um tratamento de exceção sui generis.

Funciona assim: se um método qualquer quiser lançar uma exceção, chama o método estático User::Leave(código_do_erro). Normalmente isto faz o programa fechar. Porém, se o chamador do método quiser capturar esta exceção, ele chama o método através da macro TRAPD(erro, método). Em caso de exceção, a variável erro conterá o código, e o chamador pode agir conforme.

Internamente, este mecanismo parece ser implementado usando longjmp(). CORREÇÃO: segundo o amigo Adenilson, o Symbian 9 utiliza exceções C++ padrão internamente.

Como convenção, todo método que pode causar uma exceção termina com L (de Leave, não de Loser :). Métodos como NewL, ConstructL, RunL têm esse nome pois podem "sair", e os chamadores têm de lidar com isso.

Nas exceções C++ nativas, se qualquer construtor lançar uma exceção, a memória eventualmente comprometida é automaticamente liberada. Mas a exceção Symbian é muito pobre para garantir isso. A solução foi dividir o processo de construção em duas fases: NewL e ConstrucL. NewL() cuida principalmente de alocar o objeto, lidando com a possibilidade da falta de memória. ConstructL() acaba de construir um objeto já alocado (por exemplo abrindo arquivos, conexões de rede que o objeto precise etc.).

Quem apenas usa uma classe, simplesmente chama classe::NewL() para obter um objeto. Quem implementa a classe precisa prover NewL() e ConstructL(), e invocar ConstructL() de dentro de NewL().

Um problema no tratamento de exceções de construção é a destruição de objetos em heap. Para facilitar isto, foi criada outra convenção: classes feitas para serem alocadas em heap têm o nome começando em C maiúsculo (daí os nomes CEPxApplication, CEPxAppUi...). Em tais classes, o construtor C++ deve ser privado (ou seja, proibido de ser usado diretamente) e o usuário da classe sempre chama a fábrica classe::NewLC() para obter novos objetos.

A propósito, esta é outra convenção: métodos que retornem um novo objeto em heap devem terminar com o nome em C, por isso NewL() vira NewLC(). Mas normalmente pode-se usar NewL() mesmo ppara tais classes (NewL simplesmente chama NewLC).

A outra avis-rara do Symbian é o "active object". Quase todas as operações não triviais do Symbian são assíncronas, ou seja, você chama e ela retorna imediatamente, sem o resultado que você quer. Por outro lado, não é recomendado usar threads para lidar com tais operações. A saída é usar um active object, que tenta ser uma espécie de thread "leve" (na verdade é multitarefa colaborativa).

Se você tem uma tarefa assíncrona, deve criar uma classe para ela, e esta classe deve ser descendente de CActive.Todos os active objects são controlados por um escalonador, que escolhe o active object de maior prioridade e chama RunL() para sua classe, quando nada mais importante há a ser feito.

Dentro de RunL() você faz a sua tarefa assíncrona, de preferência em pequenos pedaços e retornando para o escalonador assim que possível. Enquanto você não retornar de RunL(), o escalonador não terá chance de alocar tempo para outros active objects.

No caso de solicitar tarefas assíncronas a um "servidor", ou seja, a algum subsistema do Symbian, você terá de passar um objeto Observador, um design pattern que faz papel de callback. Normalmente isso implica que você torne uma classe do seu programa descendente do observador adequado àquele subsistema.

Se por exemplo você quer tirar uma foto, poderia fazer CEpxAppUi ser uma subclasse de MCameraObserver, e implementar o método CEpxAppUi::ImageReady(). Solicite a foto chamando CCamera::CaptureImage(), e quando ela estiver pronta, o subsistema câmera chamará seu método ImageReady().

Outro uso do observador é ser notificado quando uma tarefa delegada a um active object foi terminada. Supondo que deleguemos a gravação da foto a um active object. Essa gravação pode demorar a acontecer pois outros eventos mais prioritários acontecem (e.g. o usuário pode estar tirando mais fotos). Quando a gravação finalmente acaba, o active object chama o observador.

Parece interessante mas deixa o código extremamente engessado, devido ao observador ter de descender de uma classe. No caso de uma linguagem interpretada como Python, bastaria que o observador implementasse o método de nome FooBar(), não haveria necessidade de descender da class FooBarObserver.

Eu mencionei que o Symbian tem "servidores". Tendo seguido as modas dos anos 90, Symbian é microkernel, e cada serviço como: sistema de arquivos, rede, câmera, tela etc. é controlado por um processo separado. O kernel apenas cuida da memória virtual e coordena a troca de mensagens entre clientes e servidores. É um dos motivos pelo qual quase todo serviço é atendido assincronamente.

Parece bacana, e na verdade tem suas vantagens, mas implica que cada chamada de sistema implique em 2 trocas de contexto no mínimo (cliente para servidor e servidor para cliente) fora a "burocracia" na comunicação. Conforme os celulares ficam mais rápidos, a lentidão inerente ao Symbian fica mais aparente, em particular na interface de usuário.

2 Comentários:

Às 15:46 , Blogger Beyonlo disse...

Epx, manda tutoriais tbém de desenvolvimento avançado em python for S60 :-)

 
Às 16:57 , Blogger Adenilson disse...

Elvis

Achei o artigo bem legal, consegue apresentar boa parte das peculiaridades do Symbian de forma direta.

Somente um comentário, acho que a partir do Symbian 9.0 o mecanismo de Leave/Trap usa exceções C++ padrão, internamente.

Inclusive, vc pode lançar exceções com 'throw' (porém não objetos completos). No forum Nokia tem mais discussões a respeito.

Abraços

Adenilson

 

Postar um comentário

<< Início