[Objective-C] Categories, Static Libraries e Pegadinhas

PT | EN
23 de abril de 2011 · 💬 Participe da Discussão

Como alguns de vocês já sabem, eu tenho um pequeno pet project chamado ObjC Rubyfication, um exercício pessoal de escrever uma sintaxe parecida com Ruby dentro de Objective-C. A maior parte do projeto se aproveita do fato de que dá pra reabrir as classes padrão do Objective-C – muito parecido com Ruby, diferente do Java – e inserir nosso próprio código – através de Categories, que são parecidas com os módulos do Ruby.

A ideia desse pet project é ser uma Static Library que eu possa adicionar facilmente em qualquer outro projeto e ter todas as suas funcionalidades disponíveis. Nesse artigo eu queria mostrar como estou organizando os vários subprojetos dentro de um projeto só (e estou aberto a sugestões e dicas pra melhorar isso, já que ainda estou aprendendo a organizar as coisas dentro de projetos Obj-C) e falar sobre uma pegadinha que me tomou horas pra descobrir e que talvez ajude alguém.

Pra deixar esse exercício ainda mais divertido, eu também adicionei um target separado pra minha suíte de testes unitários (e ver como o XCode dá suporte a testes), depois outro target pro framework BDD Kiwi pra Obj-C, e mais um pro CocoaOniguruma que eu acabei de explicar no artigo anterior.

Eu venho brincando com formas de reorganizar o meu projeto e percebi que estava fazendo errado. Eu estava adicionando todos os arquivos fonte do meu target “Rubyfication” dentro do target Specs. Então tudo compilava direitinho, todas as specs passavam, mas a forma como eu definia as dependências estava errada. É meio complicado de entender no começo, mas deveria ser algo assim:

  • Target CocoaOniguruma: deveria ser uma static library, sem dependências de target e sem bibliotecas binárias pra linkar, apenas com uma dependência do framework Foundation padrão. Ele expõe os headers OnigRegexp.h, OnigRegexpUtility.h e oniguruma.h como public headers.
  • Target Kiwi: deveria ser outra static library, sem dependências de target e sem bibliotecas binárias pra linkar, apenas com as dependências dos frameworks Foundation e UIKit.
  • Rubyfication: deveria ser outra static library, com CocoaOniguruma como dependência de target, linkando contra o binário libCocoaOniguruma.a e dependendo do framework Foundation também. Ele expõe todos os seus arquivos .h como public headers.
  • RubyficationTests: deveria ser um Bundle criado junto com o target Rubyfication (você pode especificar se quer um target de testes unitários quando cria novos targets), com Kiwi e Rubyfication como dependências de target, linkando contra os binários libKiwi.a e libRubyfication.a, e os frameworks Foundation e UIKit também.

Se você ficar criando novos targets manualmente, o XCode 4 também vai criar um monte de Schemes que você nem precisa. Eu mantenho os meus limpos com apenas o scheme Rubyfication. Você pode acessar o menu “Product” e a opção “Edit Scheme”. Aí o meu Scheme fica assim:

Eu costumo configurar todos os meus build settings pra usar “LLVM Compiler 2.0” pras configurações de Debug e “LLVM GCC 4.2” pras configurações de Release (na verdade, eu faço isso por precaução porque não sei se o pessoal está realmente fazendo deploy de binários em produção compilados com LLVM).

Eu também configuro o “Targeted Device Family” pra “iPhone/iPad” e tento deixar o “iOS Deployment Target” em “iOS 3.0” sempre que possível. O pessoal normalmente deixa o padrão, que vai ser o release mais recente – atualmente 4.3. Cuidado porque o seu projeto pode não rodar em dispositivos mais antigos desse jeito.

Por fim, eu também garanto que os “Framework Search Paths” apontem pra estas opções:


“$(SDKROOT)/Developer/Library/Frameworks”  
“${DEVELOPER_LIBRARY_DIR}/Frameworks”  

Tudo compila numa boa desse jeito. Aí eu posso apertar “Command-U” (ou ir no menu “Product”, opção “Test”) pra fazer o build do target “RubyficationTests”. Ele compila todas as dependências de target, linka tudo junto e roda o script final pra executar os testes (você precisa garantir que está selecionando o “Rubyfication – iPhone 4.3 Simulator” no menu de Schemes). Ele vai abrir o Simulator pra rodar as specs.

Mas aí eu estava recebendo:


Test Suite ‘/Users/akitaonrails/Library/Developer/Xcode/DerivedData/Rubyfication-gfqxbgyxicfpxugauehktilpmwzv/Build/Products/Debug-iphonesimulator/RubyficationTests.octest(Tests)’ started at 2011-04-24 02:16:27 +0000  
Test Suite ‘CollectionSpec’ started at 2011-04-24 02:16:27 +0000  
Test Case ‘-[CollectionSpec runSpec]’ started.  
2011-04-23 23:16:27.506 otest[40298:903] [__NSArrayI each:]: unrecognized selector sent to instance 0xe51a30  
2011-04-23 23:16:27.508 otest[40298:903] ***** Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ’[__NSArrayI each:]: unrecognized selector sent to instance 0xe51a30’  

Ele diz que uma instância de NSArray não está reconhecendo o selector each: enviado pra ela no arquivo CollectionSpec. Provavelmente é esse trecho:


# import “Kiwi.h”  

# import “NSArray+functional.h”  

# import “NSArray+helpers.h”  

# import “NSArray+activesupport.h”

SPEC_BEGIN(CollectionSpec)

describe(@"NSArray", ^{  
 __block NSArray* list = nil;

context(@"Functional", ^{ beforeEach(^{ list = [NSArray arrayWithObjects:@"a", @"b", @"c", nil]; }); 

it(@"should iterate sequentially through the entire collection of items", ^{ NSMutableArray* output = [[NSMutableArray alloc] init]; 

[list each:^(id item) { [output addObject:item]; }];

[[theValue([output count]) should] equal:theValue([list count])]; });

Referência: CollectionSpec.m

Repare que na linha 3 está o import correto onde a category NSArray(Helpers) está definida com o método each: declarado certinho. O erro está acontecendo na spec da linha 18 do trecho acima.

Agora, isso não era um erro de compilação, era um erro de runtime. Então o import está achando o arquivo correto e compilando, mas algo na fase de linking não está indo bem e em runtime a category NSArray(Helpers), e provavelmente outras categories, não estão disponíveis.

Levou algumas horas de pesquisa, mas finalmente descobri uma flag simples que mudou tudo, a flag de linker -all_load. Como a documentação diz:

Importante: Para aplicações de 64-bit e iPhone OS, existe um bug no linker que impede o -ObjC de carregar arquivos de objeto de static libraries que contêm apenas categories e nenhuma classe. O workaround é usar as flags -all_load ou -force_load.

-all_load força o linker a carregar todos os arquivos de objeto de cada archive que ele encontra, mesmo aqueles sem código Objective-C. -force_load está disponível no Xcode 3.2 e posteriores. Ele permite um controle mais granular do carregamento de archives. Cada opção -force_load deve ser seguida por um caminho pra um archive, e cada arquivo de objeto naquele archive vai ser carregado.

Então todo target que depende de static libraries externas que carregam Categories tem que adicionar essa flag -all_load em “Other Linker Flags”, na categoria “Linking” dentro de “Build Settings” do target, assim:

Então tanto o meu target RubyficationTests quanto o Rubyfication tiveram que receber essa nova flag. E agora todos os testes passam sem problema!