Поддержка плагина перекрестной архитектуры

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

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

Кроме того, эта глава описывает несколько общих межпроцессных взаимодействий APIs, объясняет, как передать большие объемы данных между хост-приложением и сменным узлом помощника, и говорит, как запустить тот узел к определенной архитектуре.

Выбор модели архитектуры узла помощника

Прежде чем можно будет создать узел помощника, необходимо сначала выбрать модель архитектуры, выполняющую потребности. Существует много возможных моделей проекта для узлов помощника, каждого с переменными уровнями функциональности и трудности. В этом разделе описываются три таких модели и объясняет проблемы, с которыми можно встретиться с каждой моделью.

Из этих моделей обычно рекомендуется удаленный хостинг, потому что это самое простое и самым надежным. Когда объем API ограничивается, ограниченный маршалинг вызова функции работает. Полного программируемого маршалинга вызова функции, несмотря на то, что описано здесь, нужно обычно избегать, потому что исключения и граничные случаи могут сделать его непрактичным.

Программируемый маршалинг вызова функции

Первая вещь большинство разработчиков рассматривает выполнение, когда они разрабатывают узел помощника, пытается сделать каждый вызов функции из сменного результата в той же функции, вызываемой в узле. С программируемым маршалингом вызова функции Ваше приложение извлекает символы из плагина, затем генерирует пользовательский код библиотеки к маршальским параметрам через границы адресного пространства (или даже от одной машины до другого).

В целом чистое число исключений и граничных включенных случаев делает программируемый вызов функции, упорядочивающий очень непрактичный, и таким образом этого нужно обычно избегать. Если плагины вызывают только C APIS, Однако этот проект может быть разумным.

Такой проект, несмотря на то, что мощный, хитер для разбираний, особенно, когда используется через границы порядка байтов, потому что этот проект требует близкого понимания каждой структуры данных, включенной, чтобы знать, должно ли поле быть подкачанным байтом. Например, свопинг различного уровня BSD, объединяющего структуры данных в сеть, имел бы катастрофические последствия.

К счастью, эти типы данных являются безусловно исключением, а не правилом, и могут обычно игнорироваться. Однако распространенное использование сокрытия структуры (например, использование void * указатели и непрозрачные типы), по существу делает программируемый вызов функции, упорядочивающий почти невозможный, потому что нет никакого способа программно решить, что глубинная структура части данных передала этим способом (и во многих случаях, значение может быть бессмысленным в контексте различного процесса).

Если плагин выполняет некоторый код в локальном процессе и передает получающиеся данные тесно связанным функциям в удаленном процессе, непрозрачные структуры данных являются особенно проблемой. Например, ссылки файловой системы (FSRef) не были бы целесообразны, когда передано через IPC к процессу, работающему на различной архитектуре из-за различий в порядке байтов в базовой (непрозрачной) структуре. Точно так же дескрипторы файлов (POSIX) функционируют по-другому в зависимости от приложения, и таким образом не могут быть полезно переданы через IPC.

Если плагин должен вызвать произвольный C++ или методы класса Objective C или методы экземпляра для классов вне самого плагина, становится еще более трудным удаленно выполнить вызовы функции из-за потребности поддержать синхронизацию экземпляров класса между узлом помощника и основным узлом. Так как Вы не можете перекомпилировать плагин, замена переменных с методами доступа невозможна. Это означает, что каждая функция, потенциально управляющая состоянием, должна скопировать все состояние от понятия основного узла экземпляра класса. Далее, существует все еще некоторая возможность, что узел мог обновить общедоступные элементы класса без Вашего ведома, приведя к потенциально существенным изменениям в Вашем хост-приложении.

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

Ограниченный маршалинг вызова функции

Ограниченный маршалинг вызова функции является намного более реалистическим подходом, чем полностью программируемый маршалинг. Во-первых, идентифицируйте ряд (обычно C) подпрограммы, перезванивающие в хост-приложение. Затем замените тех в узле помощника с библиотеками, вызывающими через границы адресного пространства.

Поскольку объем поддерживаемого API ограничивается, это намного более практично для Вас для поддержки их посредством маршалинга вызова функции, потому что Вы можете подпрограммы ручного кода для каждой функции или класса, который Вы намереваетесь вызвать через границу адресного пространства вместо того, чтобы полагаться на программно сгенерированные функции и классы.

Как с программируемым маршалингом вызова функции, необходимо быть особенно осторожными при работе с указателями. Если параметры указателя имеют известный тип и размер, относительно просто работать с ними. Однако можно встретиться с проблемами, если Вы не знаете размер ссылочного объекта и если Вы должны к байту подкачать или иначе управлять содержанием указателя во время граничного пересечения.

C++ и классы Objective C немного более тверды. Вы не можете просто передать указатели на классы, потому что они не будут допустимы с другой стороны канала передачи. Однако, если число классов ограничивается, можно эмулировать указатели класса при помощи тупиковых классов в узле помощника, содержащих дополнительную задействованную переменную, хранящую адрес реального экземпляра класса на стороне узла.

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

При использовании ограниченного маршалинга вызова функции узел помощника может быть очень компактным и абсолютно прозрачным. Однако Ваши тупиковые библиотеки должны содержать каждую функцию, которую Вы намереваетесь переопределить. Если Вы не контролируете API сам, для большого сменного APIs этот подход может быть пугающим, особенно.

Удаленный хостинг

Удаленный хостинг строго рекомендуется для большинства реализаций хостов помощника, потому что относительно просто реализовать надежно. С удаленным хостингом, вместо того, чтобы полагаться на знание сменной архитектуры, Вы полагаетесь на свое знание самого сменного узла. Поскольку Вы управляете рассматриваемым кодом, Вы будете знать о любых изменениях в API. Кроме того, потому что интерфейс между узлом и его встроенным механизмом редко включает указатели обратного вызова, можно использовать более простой механизм связи.

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

Можно принять решение добавить флаг командной строки (использование argc и argv) к Вашему приложению и, после наблюдения того флага, вызывают отдельную подпрограмму инициализации, в которой только сконфигурирована внутренняя функциональность. Если Вы делаете, Ваш узел помощника может просто быть другим рабочим экземпляром Вашего двоичного файла главного приложения.

Самое большое изменение, которое необходимо внести для поддержки удаленного хостинга, должно поддержать состояние сменного механизма поддержки посредством вызовов функции вместо переменного присвоения (если Вы уже не делаете так). После внесения того изменения всюду по узлу проблема становится относительно простым набором изменений в этих функциях:

  • Изменения состояния к сменному уровню хост-приложения должны быть отражены в узле помощника.

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

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

Эта синхронизация состояния может быть достигнута посредством относительно прямого использования межпроцессного взаимодействия (обсужденный в Использовании Межпроцессного взаимодействия). Для транспорта больших данных необходимо обычно передавать данные с помощью отображения памяти. Этот метод описан в Памяти, Отображающейся для Объемного Транспорта Данных.

Используя межпроцессное взаимодействие

Для межпроцессного взаимодействия можно разработать узел помощника с помощью трех широких моделей:

Вызов удаленной процедуры APIs

Существует три вызова удаленной процедуры, обычно использующиеся в OS X: распределенные объекты, Мах RPC и Sun RPC. Из них только распределенными объектами является общедоступный API, рекомендуемый для общего использования.

XPC Services

В OS X v10.7 и позже, службы XPC являются рекомендуемым способом поддерживать межпроцессное взаимодействие. API служб XPC позволяет Вам превратить межпроцессные вызовы метода в объекты, живущие в различном адресном пространстве, прозрачно упорядочивая данные к дочернему процессу и назад.

Чтобы изучить, как создать службу XPC, считайте Руководство по программированию Демонов и Служб.

Распределенные объекты

Если XPC не доступен на Вашем целевом OS, и если Ваше программное обеспечение не должно работать в песочнице, Распределенные Объекты могут обеспечить схожую функциональность.

При вызове C или C++ APIs необходимо обернуть их в классы Objective C, прежде чем можно будет использовать этот API. Перед рассмотрением выполнения так необходимо считать Ограниченный Маршалинг Вызова функции.

Для получения дополнительной информации о распределенных объектах посмотрите Распределенные Объекты Программировать Темы.

Мах RPC

Маху RPC не считают открытым интерфейсом и его прямым использованием, обычно не рекомендуют. Однако, если Вы решаете использовать его, можно найти информацию об этом в следующем URLs:

Sun RPC

Sun RPC выходит за рамки этого документа. Можно найти больше информации в следующих местах:

  • rpc страница справочника

  • rpcgen страница справочника

  • xdr страница справочника

  • rpcinfo страница справочника

  • portmap страница справочника

Sun RPC обычно не рекомендуют для новых проектов.

Клиент/сервер, Передающий APIs

OS X поддерживает несколько клиентов/серверов, передающих APIs, включая события Apple, сокеты BSD и каналы (стандартный ввод и вывод, например). Этот APIs описан в следующих разделах.

События Apple

Общий API для межпроцессного взаимодействия в OS X является событиями Apple. Событиями Apple API является довольно прямой API для низкой пропускной способности IPC, и Вы, вероятно, уже используете ее в своем приложении. Если так, можно добавить дополнительные типы сообщений для коммуникации между приложением и сменным узлом.

Для получения дополнительной информации см. Руководство по программированию Событий Apple.

Программирование сокета

Наиболее распространенный API для простого межпроцессного взаимодействия является старым резервным устройством, сокетами. Существует много различных технологий OS X для работы с сокетами, включая:

Каждый этот APIs реализует то же базовое сообщение, двунаправленный поток байтов между обоими концами. Обмен сообщениями на основе потоков представляет проблему, если Ваш узел помощника должен одновременно поддерживать многократные плагины, однако, потому что необходимо будет мультиплексировать данные из многократных источников. Можно решить эту проблему при помощи очередей сообщений, как описано в Очередях сообщений.

Как добавленный бонус, коммуникация через сокеты не ограничивается единственной машиной. Если Вы пишете программное обеспечение, которое может получить преимущества от распределенных вычислений, такая удаленная связь может быть значительным преимуществом.

Если Вы пишете узел программы звукового сопровождения, большая часть работы выполнена для Вас начинающийся в OS X v10.4. Аудиоустройства AUNetSend и AUNetReceive могут сделать помощника, размещающего относительно безболезненный для реализации, ли на локальной машине или удаленно. Даже если место назначения находится на локальной машине, Однако с этими плагинами, вся информация проходит через штабель TCP/IP.

Следует иметь в виду два протеста при использовании TCP/IP для передачи фактических данных назад и вперед вместо того, чтобы просто передать управляющую информацию. Во-первых, задержка удаленных связей является весьма значительной. Если это имеет значение в Вашем приложении (например, аудиоприложение), необходимо компенсировать эту задержку, или качество пострадает значительно. Во-вторых, сумма отправляемой информации является существенной, и таким образом по причинам производительности, программирование сокета может не быть идеальным для хостинга большого количества отдельных плагинов на узле помощника. Если Вы ожидаете большое количество несобственных плагинов, необходимо обычно использовать память, отображающуюся для передачи данных от основного узла до узла помощника, как описано в Памяти, Отображающейся для Объемного Транспорта Данных.

С теми протестами в памяти, сокеты также открывают возможность альтернативных моделей использования программного обеспечения. Например, Вы могли бы разработать аудиоприложение так, чтобы фронтэнд мог работать на небольшой, низкой мощности, fanless компьютер в студийной диспетчерской, со всем тяжелым подъемом, выполняемым отдельным компьютером в другой комнате. Вы могли реализовать пользовательский интерфейс путем временного хостинга локальной копии плагинов, когда пользователь хочет показать их пользовательский интерфейс, затем отправляя сообщения изменения управления через провод к фактическому узлу (куда плагины все работают без выведенного на экран UI). Затем используйте TCP/IP для отправки только необработанного аудио от аудиоинтерфейса. Для аудио игры - через при записи, необходимо смешать входящее аудио в вывод на компьютере фронтэнда как самый последний шаг в обработке.

Подробные данные создания и использования сокетов выходят за рамки этого документа. Для получения дополнительной информации консультируйтесь с упомянутыми выше документами. Можно также счесть полезную информацию в Сокете UNIX FAQ, который может быть найден в http://www .developerweb.net/forum/. Этот FAQ включает примеры кода, иллюстрирующие, как использовать сокеты домена TCP/IP и UNIX на уровне API BSD.

Стандартный ввод и вывод

Другой общий API для межпроцессного взаимодействия является стандартным вводом и выводом. Этот API обеспечивает пару однонаправленных потоков. Во многом как программирование сокета природа на основе потоков стандартного ввода и вывода требует, чтобы Вы хранили дополнительную информацию состояния, если необходимо связать ответы на сообщения с исходным сообщением. Хороший способ решить ту проблему с помощью очередей сообщений, как описано в Очередях сообщений.

Одна вещь, делающая стандартный ввод и вывод удобным, состоит в том, что они в основном устанавливаются для Вас. Каждый процесс в основанной на UNIX системе имеет стандартный ввод и вывод автоматически. Можно использовать в своих интересах их для передачи между родительским процессом (главное приложение) и его дочерними элементами (узел помощника).

Для передачи с дочерними процессами в Какао необходимо использовать NSTask API, описанный в Ссылке класса NSTask. Для получения дополнительной информации об этом методе считайте Создание и Запуск NSTask и Окончание NSTask.

Также в инструментах BSD, можно выполнить ту же вещь на уровне дескриптора файла с помощью нескольких низкоуровневых APIs как показано в этом примере:

#include <stdlib.h>
comm_channel *startchild(char *path)
{
        comm_channel *channel = malloc(sizeof(*channel));
        pid_t childpid;
        int in_descriptors[2];
        int out_descriptors[2];
 
        /* Create a pair of file descriptors to use for communication. */
        if (pipe(in_descriptors) == -1) {
                fprintf(stderr, "pipe creation failed.\n");
                goto error_exit;
        }
        if (pipe(out_descriptors) == -1) {
                fprintf(stderr, "pipe creation failed.\n");
                goto error_exit;
        }
        /* Create a new child process. */
        if ((childpid = fork()) == -1) {
                fprintf(stderr, "fork failed.\n");
                goto error_exit;
        }
        if (childpid) {
                /* Parent process */
                channel->in_fd = in_descriptors[0];
                close(in_descriptors[1]);
                channel->out_fd = out_descriptors[1];
                close(out_descriptors[0]);
                return channel;
        } else {
                /* Child process */
                if (dup2(in_descriptors[1], STDOUT_FILENO) == -1) {
                        fprintf(stderr, "Call to dup2 failed.\n");
                        goto error_exit;
                }
                close(in_descriptors[0]);
 
                if (dup2(out_descriptors[0], STDIN_FILENO) == -1) {
                        fprintf(stderr, "Call to dup2 failed.\n");
                        goto error_exit;
                }
                close(out_descriptors[1]);
 
                execl(path, path, NULL);
 
                /* If we get here, something went wrong. */
                fprintf(stderr, "Exec failed.\n");
                goto error_exit;
        }
        return channel;
    error_exit:
        free(channel);
        perror("msg_send");
        return NULL;
}

Очереди сообщений

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

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

На каждом конце у Вас должен быть поток для обработки сообщений от сокета. Можно использовать поток цикла выполнения в качестве потока обработчика, если Вы пишете традиционное приложение, или можно использовать отдельный поток сообщения, если Вы предпочитаете использовать сокет низшего уровня APIs.

Код для управления очередью сообщений является относительно прямым, блокируя проблемы несмотря на это. Полный пример кода предоставлен в сопутствующих файлах, связанных с этим документом. Сопутствующий архив файлов может быть загружен с боковой панели при просмотре этого документа как HTML в Ссылочной Библиотеке ADC (developer.apple.com).

Память, отображающаяся для объемного транспорта данных

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

Следующий пример показывает, как создать область общей памяти между процессом и его дочерним элементом:

#include <sys/types.h>
#include <sys/mman.h>
#include <sys/dirent.h>
#include <fcntl.h>
#include <stdlib.h>
 
/* Create the map file and fill it with bytes. */
char *create_shm_file(char *progname, int length)
{
    int fd, i;
    char *filename=malloc(MAXNAMLEN+1);
    char *ret;
    char byte = 0;
 
    sprintf(filename, "/tmp/%s-XXXXXXXX", progname);
    ret = mktemp(filename);
 
    fd = open(filename, O_RDWR|O_CREAT, 0600);
    for (i=0; i<length; i++) {
        write(fd, &byte, 1);
    }
    return ret;
}
 
/* Map the file into memory in a read-write fashion */
void *map_shm_file(char *filename, int length)
{
    int fd = open(filename, O_RDWR, 0);
    void *map;
    if (fd == -1) return NULL; /* Could not open file */
    map = mmap(NULL, length, PROT_READ|PROT_WRITE,
        MAP_FILE|MAP_SHARED, fd, 0);
    return map;
}

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

Если Вы намереваетесь работать с областями размера страницы, необходимо также принять во внимание функции, описанные в mpool страница руководства. Если необходимо регулярно выделять и освобождать общую память, Однако в большинстве целей, необходимо записать собственное средство выделения пула.

Для получения дополнительной информации о функциях, используемых в примере выше, см. страницы справочника для mmap, open, и mktemp.

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

Например:

typedef struct ringbuffer {
    void *buffer;
    int buflen;
    int readpos;
    int writepos;
} *ringbuffer;
 
#define BYTES_TO_READ(ringbuffer) (ringbuffer->writepos - \
    ringbuffer->readpos + \
    ((ringbuffer->readpos > ringbuffer->writepos) * \
        (ringbuffer->buflen)))
 
/*  Use >= here because if readpos and writepos are equal,
    the buffer must be assumed to be empty.  Otherwise,
    the buffer would start out full. For this reason,
    the writepos must not be allowed to overtake the read
    position, so subtract one from the final value.
*/
#define BYTES_TO_WRITE(ringbuffer) (ringbuffer->readpos - \
    ringbuffer->writepos + \
    ((ringbuffer->writepos >= ringbuffer->readpos) * \
        ringbuffer->buflen) - 1)

Чтение кода от этого буфера знает, что может всегда читать из readpos вперед до writepos (или если writepos меньше, чем readpos, это может читать до конца буфера, затем читать из запуска буфера до writepos). После чтения, обновлений кода чтения readpos отразить расположение последнего побайтового чтения.

Подобным способом код, пишущий в этот буфер, знает, что это может безопасно записать из writepos позиция, пока это не достигает readpos, перенос конца буфера при необходимости. После записи, обновлений кода записи writepos отразить расположение последнего записанного байта.

Поскольку только один процесс будет когда-либо изменять также readpos или writepos, никакая синхронизация между двумя процессами не требуется. Отметьте, однако, что код чтения должен защитить readpos против других потоков в том процессе и кода записи должен сделать то же для writepos.

Запуск узла помощника

После того, как Вы имеете, создают узел помощника, следующий шаг должен определить архитектуру плагина. Для плагинов PEF/CFM безопасно для Вас предположить, что плагин содержит 32-разрядный исполняемый код PowerPC. Для Мужественных плагинов метод, который необходимо использовать, варьируется согласно версии используемого OS X.

Для обратной совместимости с версиями OS X до 10,5, Ваше приложение должно использовать код обнаружения, представленный в примере кода CheckExecutableArchitecture. Этот пример кода является прямым и представляет довольно простой способ определить который архитектура использовать для загрузки существующих плагинов.

В OS X v10.5 и позже, необходимо использовать CFBundle API. Этот API более безопасен как долгосрочное решение, потому что оно будет поддерживать любой двоичный формат, поддерживающийся той определенной версией OS X, таким образом освобождая Вас от потребности изменить код, поскольку представлены новые двоичные форматы. Соответствующие функции:

CFArrayRef CFBundleCopyExecutableArchitecturesForURL(CFURLRef url);
CFArrayRef CFBundleCopyExecutableArchitectures(CFBundleRef bundle);

Следующий шаг должен выполнить узел помощника, выбрав надлежащую архитектуру в процессе. В OS X v10.5 и позже, рекомендуемый способ запустить исполнимую программу с помощью определенной архитектуры посредством расширения posix_spawn. Этот API описан в странице руководства для posix_spawn, posix_spawnattr_init, и связанные страницы руководства соединились от тех страниц. Расширение для выбора архитектуры для запуска описано в странице руководства для posix_spawnattr_setbinpref_np.

Для поддержки узлов помощника на OS X v10.4 можно использовать отдельные копии узла помощника к каждой архитектуре процессора вместо универсального двоичного файла, затем запуститься, какой бы ни версия является надлежащей.