Система сборки Objective-C, которая может делать модули - PullRequest
4 голосов
/ 31 декабря 2011

Я работаю над небольшим моим хобби-проектом, в котором у меня есть большая структура, которая выполняет все основные задачи. Это ядро ​​само по себе ничего не даст, ему нужна дюжина подсистем , которые действительно выполняют тяжелую работу. В настоящее время я написал только одну подсистему , поэтому все еще легко изменить.

У меня есть много мест кода, где ядро ​​взаимодействует с подсистемами , и я не хочу менять ядро ​​каждый раз, когда добавляю новую подсистему . Моя идея состояла в том, чтобы сделать его модульным.

Долгое время я видел нечто подобное в игровом движке, где новую консольную команду можно было определить с помощью некоторого макроса препроцессора. Это было все, что вам нужно было сделать - после компиляции он мгновенно заработал в игре.

Итак, давайте возьмем игровой движок в качестве примера для моего случая. Я разместил комментарии в приведенном ниже коде, которые должны сделать мой вопрос более очевидным.

Мой вопрос: как мне реализовать модульную систему в Objective-C, которая создается во время компиляции и не требует изменения ничего, кроме самих модулей?

А теперь какой-нибудь код

-(void)interpretCommand:(NSString*)command {
    // Find the space in the command
    NSRange pos = [command rangeOfString:@" "];
    if (pos.length == 0) return; // No space found

    NSString *theCommand = [command substringToIndex:pos.location];

    // !!! Here comes the important code !!!
    // Get all the available commands - this is what my question is about!
    NSDictionary *allCommands = nil;

    // Find the command in the dictionary
    NSString *foundCommand = [allCommands objectForKey:theCommand];

    // Execute the command
    if (foundCommand != nil) {
        [[NSClassFromString(foundCommand) new] execute];
    }
}

Я хочу добавить новую команду, например:

REGISTER_COMMAND(MyClassName, "theCommand")

Помните, приведенный выше код не является моим конкретным случаем. Кроме того, я не хочу внешние модули, они должны быть скомпилированы, как если бы они были реализованы изначально . Objective-C в порядке, так же как C ++ или C.

Обновление
Пояснение: я знаю, как сделать это с помощью plist-файла, но если бы я выбрал это, я мог бы также сохранить их в моем реальном коде. Я ищу решение C / C ++ / Objective-C, которое позволяет мне просто добавить модуль с макросом препроцессора.

Обновление 2
Добавление награды - мне бы очень хотелось получить несколько хороших идей для этого.

Ответы [ 7 ]

5 голосов
/ 02 января 2012

Я не до конца понимаю проблему.Тем не менее, из того, что я могу понять, камнем преткновения является нахождение ловушки во время выполнения, чтобы вы могли зарегистрировать свои модули.

Одним из таких ловушек является метод класса +(void)load.load вызывается для каждого загруженного класса и категории.Для статически связанных классов / категорий это будет, когда ваше приложение запускается.Даже если вы решите не использовать Objective-C, вы все равно можете создать класс просто для ловушки, которую обеспечивает его метод 'load.

3 голосов
/ 02 января 2012

Это похоже на то, как писал @verec: Вы можете добавить специальный класс в свой проект, который называется ModuleList.Каждый модуль, желающий зарегистрироваться сам, может сделать это, добавив категорию к ModuleList.Вы можете поместить это в макрос.Используя функции objc / runtime, вы можете перебирать добавленные свойства или методы.(т. е. все свойства / методы, которые не происходят из NSObject)

Преимущество состоит в том, что вам не нужно перебирать все классы.

2 голосов
/ 08 января 2012

Хорошо, если это должно быть макросом, это решение работает:

    // the macro:
#define REGISTER_COMMAND( __THE_CLASS, __COMMAND )\
@interface __THE_CLASS##Registration @end\
@implementation __THE_CLASS##Registration \
+(void)load { [ Commands registerHandler:NSClassFromString(@""#__THE_CLASS) command:(__COMMAND) ] ; } \
@end

    // Bookkeeping/lookup class:
@interface Commands 
@end

@implementation Commands
static NSMutableDictionary * __commands = nil ;

+(void)load
{
    __commands = [[ NSMutableDictionary alloc ] init ] ;
}

+(void)registerHandler:(Class)theClass command:(NSString*)command
{
    if ( theClass && command.length > 0 )
    {
        [ __commands setObject:theClass forKey:command ] ;
    }
}

+(id)handlerForCommand:(NSString*)command
{
    Class theClass = [ __commands objectForKey:command ] ;
    return [ [ [ theClass alloc ] init ] autorelease ] ;
}

@end

    // map the command 'doit' to handler 'MyCommand', below
REGISTER_COMMAND( MyCommand, @"doit" )

    // one of our command handling objects, 'MyCommand'
@interface MyCommand : NSObject
@end

@implementation MyCommand
@end

    // test it out:
int main (int argc, const char * argv[])
{   
    @autoreleasepool 
    {
        NSLog(@"command %@ found for 'doit'\n", [ Commands handlerForCommand:@"doit" ] ) ;

    }
    return 0;
}
2 голосов
/ 02 января 2012

Я добавляю еще один ответ, чтобы уточнить то, что было сказано в чате выше, поскольку это может оказаться полезным для тех, у кого есть похожая проблема.

Предположение, на которое опирается это решение:

  • все модули и ядро ​​являются частью одного и того же исполняемого файла и скомпилированы и связаны вместе, как в любом стандартном проекте, и
  • существует соглашение об именах между командами и именами классов модулей.

Имея это в виду, следующий код (в "ядре") возвращает список всех классов в исполняемом файле, чья мама соответствует заданному префиксу:

#import <objc/runtime.h>

- (NSSet *) findAllClassesWithPrefix: (NSString *) prefix {
    NSMutableSet * matches = [NSMutableSet setWithCapacity:2] ;
    size_t classCount = 0 ;
    Class * classes = 0 ;

    Class nsObjectClass = objc_getClass("NSObject") ;

    classCount = (size_t) objc_getClassList(0, 0) ;

    if (classCount > 0) {
        classes = (Class *) calloc(classCount, sizeof(Class)) ;
        classCount = (size_t) objc_getClassList(classes, (int) classCount) ;

        for (int i = 0 ; i < classCount ; ++i) {
            Class  c = classes[i] ;
            if (c == nil) {
                continue ;
            } else {
                // filter out classes not descending from NSObject
                for (Class superClass = c ; superClass ; superClass = class_getSuperclass(superClass)) {
                    if (superClass == nsObjectClass) {
                        const char * cName = class_getName(c) ;
                        NSString * className = [NSString stringWithCString:cName
                                                                  encoding:NSUTF8StringEncoding] ;                        
                        if ([className hasPrefix: prefix]) {
                            [matches addObject: className] ;
                        }
                    }
                }
            }
        }

        free(classes) ;
    }

    return matches ;
}

Теперь, чтобы получить всеклассы, чьи имена начинаются с «PG»:

NSSet * allPGClassNames = [self findAllClassesWithPrefix:@"PG"] ;

for (NSString * string in allPGClassNames) {
    NSLog(@"found: %@", string) ;
}

печатает:

2012-01-02 14:31:18.698 MidiMonitor[1167:707] found: PGMidiDestination
2012-01-02 14:31:18.701 MidiMonitor[1167:707] found: PGMidi
2012-01-02 14:31:18.704 MidiMonitor[1167:707] found: PGMidiConnection
2012-01-02 14:31:18.706 MidiMonitor[1167:707] found: PGMidiAllSources

Используя простое соглашение, что команды и имена классов модулей совпадают, тогда это все, что нужноit.

Другими словами, чтобы добавить новый модуль, просто добавьте его источники классов в проект, и все готово.Среда выполнения в ядре просто подхватит его, при условии, что имя «основного класса» модуля соответствует любому соглашению об именах, которое вы установили между «командами» и именами классов модулей.

, т.е. нет необходимости в

REGISTER_COMMAND(MyClassName, "theCommand")

в любом коде модуля, и нет необходимости в любом макросе.

2 голосов
/ 31 декабря 2011

Я делаю что-то вроде этого в новом проекте, над которым я работаю. Я храню информацию о модулях (классах) в XML-файле (plist также будет хорошо работать), включая возможности класса, его имя и т. Д. Во время выполнения я загружаю XML-файл, а когда требуется определенный класс, я создать его на основе его имени. Для простоты использования / хорошей инкапсуляции у меня есть класс BaseModule, от которого наследуются «классы модулей». BaseModule имеет initWithModuleName: метод, который принимает имя модуля (как указано в файле XML):

- (id)initWithModuleName:(NSString *)moduleName
{
    if ( ![self isMemberOfClass:[BaseModule class]] ) 
    {
        THROW_EXCEPTION(@"MethodToBeCalledOnBaseClassException", @"-[BaseModule initWithModuleName] must not be called on subclasses of BaseModule.");
    }

    [self release]; // always return a subclass
    self = nil;

    if ([BaseModule canInitWithModuleName:moduleName]) 
    {
        ModuleDefinition *moduleDefinition = [BaseModule moduleDefinitionForModuleName:moduleName];

        Class moduleClass = NSClassFromString(moduleDefinition.objectiveCClassName);
        self = [(BaseModule *)[moduleClass alloc] initWithModuleDefinition:moduleDefinition];
    }

    return self;
}

В этой системе гораздо больше, чем я упомянул здесь, и это основано на моем коде, но не копируется / вставляется из него. Во-первых, я использую способность Objective C выполнять поиск имени метода во время выполнения и динамическую диспетчеризацию, чтобы вызвать методы модуля, которые объявлены / определены в подклассах BaseModule, но не в самом BaseModule. Эти методы описаны в файле XML.

Но конечный результат заключается в том, что все, что мне нужно сделать, чтобы добавить новый модуль, - это создать его определение в файле «ModuleDefinitions.xml» в моем проекте и добавить классы реализации для него в проект. Остальная часть программы автоматически обнаружит его наличие и начнет использовать его.

1 голос
/ 08 января 2012

aaaand .. здесь другое решение, основанное на загрузке / инициализации, без макроса - если вы подклассите модуль, ваш обработчик команд будет выбран во время загрузки. Вы могли бы, вероятно, заставить это работать на основе вашего класса, реализующего некоторый протокол, если бы вы хотели ...

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

//--------------------------------------------------------------------------------

@interface Module : NSObject

+(Module*)moduleForCommand:(NSString*)command ;
+(void)registerModuleClass:(Class)theClass forCommand:(NSString*)command ;


+(NSString*)command ;   // override this to returnthe command your class wants to handle

@end

@implementation Module

static NSMutableDictionary * __modules = nil ;
+(void)load
{
    @autoreleasepool 
    {
        __modules = [[ NSMutableDictionary alloc ] init ] ;
    }
}

+(void)initialize
{
    [ super initialize ] ;
    if ( self == [ Module class ] )
    {
        unsigned int count = 0 ;        
        Class * classList = objc_copyClassList( & count ) ;
        for( int index=0; index < count; ++index )
        {
            Class theClass = classList[ index ] ;
            if ( class_getSuperclass( theClass ) == self )
            {
                [ Module registerModuleClass:theClass forCommand:[ theClass command ] ] ;
            }
        }
    }
}

+(Module*)moduleForCommand:(NSString*)command
{
    Class theClass = [ __modules objectForKey:command ] ;
    return !theClass ? nil : [ [ [ theClass alloc ] init ] autorelease ] ;
}

+(void)registerModuleClass:(Class)theClass forCommand:(NSString*)command
{
    [ __modules setObject:theClass forKey:command ] ;
}

+(NSString *)command
{
    NSLog(@"override +command in your Module subclass!\n") ;
    return nil ;
}
+(BOOL)shouldLoad
{
    return YES ;    // override and set to NO to skip this command during discovery
}

@end

//--------------------------------------------------------------------------------

@interface MyModule : Module 
@end
@implementation MyModule

+(NSString *)command
{
    return @"DoSomething" ;
}
@end

//--------------------------------------------------------------------------------
int main (int argc, const char * argv[])
{
    @autoreleasepool 
    {       
        Module * m = [ Module moduleForCommand:@"DoSomething" ] ;
        NSLog( @"module for command 'DoSomething': found %@\n", m ) ;

    }
    return 0;
}
1 голос
/ 02 января 2012

Если мы начнем с вашего interpretCommand примера кода, ключевой структурой будет словарь allCommands.

Вы ищете какие-то средства для его заполнения, чтобы при запросе строки (ваша команда)он возвращает другую строку, которая будет именем класса классаjective-c, для которого вы затем можете создавать экземпляры.

Как вы хотите, чтобы ваш словарь заполнялся?

Если это во время компиляциилибо вы сами, либо в каком-то файле, либо в каком-либо инструменте, который вы пишете, придется написать немного исходного кода, который вставит

[allCommands addObject: @"ThisNewClass" forKey: @"ThisNewCommand"] ;

Если это все, что вам нужно, то вам даже не нужен макрос,но какой-то класс реестра, единственной задачей которого является заполнение вашего словаря любой новой парой класс / команда, которую вы хотите добавить.

Что-то более простое, например:

@interface Registry : NSObject {
    NSMutableDictionary * allCommands ;
}

- (id) init ;
- (NSDictionary *) allCommands ;

@end 

@implementation Registry

- (id) init {
    if (self = [super init]) {
        allCommands = [[NSMutableDictionary alloc] initWithCapacity: 20] ;

        // add in here all your commands one by one
        [allCommands addObject: @"ThisNewClass" forKey: @"ThisNewCommand"] ;
        [allCommands addObject: @"ThisNewClass2" forKey: @"ThisNewCommand1"] ;
        [allCommands addObject: @"ThisNewClass3" forKey: @"ThisNewCommand2"] ;
        [allCommands addObject: @"ThisNewClass3" forKey: @"ThisNewCommand3"] ;
    }
    return self ;
}

- (NSDictionary *) allCommands {
    return allCommands ;
}
@end

Если это не такответ, который вы ищете, не могли бы вы уточнить и указать, как этот пример не совсем соответствует вашему вопросу?

...