libkern Время выполнения C++

Когда они разработали ядро OS X, инженеры Apple выбрали ограниченную форму C++, потому что они чувствовали исключенные функции — исключения, множественное наследование, шаблоны, и информация о типах во время выполнения (RTTI) — была или недостаточна или не достаточно эффективна для высокоэффективного, многопоточного ядра. Но потому что некоторая форма RTTI требовалась, Apple разработал улучшенную систему типов во время выполнения. Эта система, реализованная libkern библиотекой, обеспечивает следующие функции:

Основной агент позади этих функций является классом OSMetaClass libkern. OSMetaClass является равноправным классом OSObject — класс, который все драйверы устройств в конечном счете получают из — потому что оба класса непосредственно наследовались от того же истинного корневого класса, OSMetaClassBase. Эта глава исследует в некоторой глубине APIs и службы классов OSMetaClass и OSMetaClassBase и обсуждает, как можно лучше всего использовать в своих интересах их в коде. APIs OSMetaClass и OSMetaClassBase определяется в OSMetaClass.h.

Создание системы во время выполнения

libkern библиотека создает и обновляет свою систему времени выполнения C++ каждый раз, когда расширения ядра загружены (или разгружены) от ядра. Каждый класс в libkern или библиотеках I/O Kit является самостоятельно объектом типа OSMetaClass. Класс OSMetaClass указывает четыре переменные экземпляра для охарактеризования экземпляров класса:

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

Рисунок 1-1 иллюстрирует, как libkern время выполнения пишет информацию о классе в эти словари, когда KEXTs загружаются в ядро.

Рисунок 1-1  , Регистрирующий информацию о метаклассе от двоичного файла KEXT
Registering metaclass information from a KEXT binary

Существует три отличных фазы к регистрации метакласса, которую загрузчик выполняет при загрузке расширения ядра:

  1. Прежде, чем Загрузить загрузчик вызывает функцию членства OSMetaClass preModLoad. Эта функция захватывает блокировку для гарантии однопоточности во время загрузки многократных расширений ядра. Это также генерирует глобальную информацию о классах в двоичных файлах KEXT в подготовке к регистрации.

  2. Загрузка загрузчика вызывает каждого из статических конструкторов, создаваемых макросами OSMetaClass как часть определения класса (seeObject Создание и Уничтожение). В результате конструктор OSMetaClass для каждого класса вызывается; параметрами за этого конструктора являются три из элементов данных OSMetaClass: Имя класса, указатель на базовый класс и размер класса. Загрузчик обновляет класс и словари модуля, с помощью KEXT-двоичного имени и имен классов как ключи и вставляя просто создаваемые объекты OSMetaClass в те словари. Это также соединяет все указатели наследования базового класса.

  3. После Загрузки После того, как вызывают всех статических конструкторов, загрузчик вызывает функцию членства OSMetaClass postModLoad. Эта функция выпускает блокировку и возвращает код результата из загрузки. Если этот код указывает ошибку, такую как плохо сформированный конструктор, попытка загрузки прерывается.

Каждый раз, когда код ядра создает экземпляр OSObject-производного-класса, libkern система типов во время выполнения постепенно увеличивает счетчик экземпляра связанного объекта OSMetaClass; это постепенно уменьшает счетчик каждый раз, когда освобожден экземпляр. Система во время выполнения использует этот рабочий счет экземпляров для предотвращения разгрузки расширений ядра, имеющих «живые» объекты в системе. (Конечно, если код неправильно сохраняет и выпускает объекты, это могло бы привести к отставанию KEXTs, который должен быть разгружен — и, следовательно, утечки памяти. И протек, ссылки на объект приводят к раздражающим и дорогостоящим задержкам цикла разработки.)

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

Создание объекта и уничтожение

Поскольку исключения исключены из ограниченной формы ядра C++, Вы не можете реализовать «нормальных» конструкторов C++ и деструкторы без опасности. Конструкторы и деструкторы вводятся для возврата значения (такого как код ошибки). Обычно, если они встречаются с проблемой, они повышают исключение. Но потому что исключения не поддерживаются во время выполнения C++ ядра, нет никакого способа для Вас знать, когда произошла ошибка выделения или освобождения.

Эта ситуация запросила конструктивную особенность системы времени выполнения C++ libkern, использующей макросы OSMetaClass для указания структуры класса — т.е. структуры данных метакласса и функциональные интерфейсы — для системы типов во время выполнения. Макросы также определяют основного конструктора и деструктор для класса. Эти макросоздаваемые конструкторы, как гарантируют, не перестанут работать, потому что они самостоятельно не выполняют выделений. Вместо этого система во время выполнения задерживает фактическое выделение объектов до их инициализации (обычно в init функция членства). Поскольку init функция вводится для возврата a bool, это позволяет возвратить ошибку на любой отказ.

Используя макросы конструктора OSMetaClass

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

Для бетона (т.е. некраткий обзор) классы, первый макрос, OSDeclareDefaultStructors объявляет конструкторов C++; условно Вы вставляете этот макрос как первый элемент объявления класса в заголовочном файле. Например:

class com_MyCompany_driver_MyDriver : public IOService
{
    OSDeclareDefaultStructors(com_MyCompany_driver_MyDriver);
    /* ... */
};

Ваша реализация класса должна включать компаньона, «определяют» макрос, OSDefineMetaClassAndStructors. Этот макрос определяет конструктора и деструктор, реализует функцию членства выделения OSMetaClass (alloc) для класса, и предоставляет информацию метакласса для системы типов во время выполнения. OSDefineMetaClassAndStructors берет в качестве параметров имя Вашего водительского класса и имя его базового класса. Это использует их для генерации кода, позволяющего классу драйвера быть загруженным и инстанцированным, в то время как работает ядро. Это обычно происходит как один из первых операторов реализации класса. Например, MyDriver.cpp мог бы начаться как это:

#include "MyDriver.h"
 
// This convention makes it easy to invoke base class member functions.
#define super    IOService
 
// You cannot use the "super" macro here, however, with the
//  OSDefineMetaClassAndStructors macro.
OSDefineMetaClassAndStructors(com_MyCompany_driver_MyDriver, IOService);

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

OSDefineMetaClassAndStructors и OSDefineMetaClassAndAbstractStructorsмакросы основываются на двух других макросах OSMetaClass: OSDefineMetaClassAndStructorsWithInit и OSDefineMetaClassAndAbstractStructorsWithInit, соответственно. Эти последние два макросы осуждаются и не должны использоваться непосредственно.

Выделение объектов динамично

OSMetaClass играет важную роль в libkern время выполнения C++ путем выделения объектов, основанных на типе класса. Производные классы OSMetaClass могут сделать это динамично путем реализации allocфункция; тип класса предоставляется самим производным классом OSMetaClass. Как упомянуто в предыдущем разделе, Области объекта и Вызове конструктора, конструкторах, создаваемых реализацией макросов OSMetaClass alloc автоматически для Вашего класса.

Контейнерные классы libkern и семьи Набора I/O обеспечивают различные функции членства помощника для создания (выделения) объекты, и это то, что Вы используете большую часть времени. Но можно также непосредственно выделить экземпляр любого libkern или класса Набора I/O с помощью двух видов вызовов OSMetaClass:

  • Путем вызова одного из OSMetaClass allocClassWithNameфункции, предоставляя идентификацию типа класса (как OSSymbol, OSString или струна до)

  • Путем вызова макроса OSTypeAlloc (определенный в классе OSMetaClassBase)

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

OSTypeAlloc макрос предназначается для замены C++ new оператор, когда Вы создаете объекты, полученные из OSObject. Причиной позади этого изменения является совместимость на уровне двоичных кодов: new оператор хрупок, если Вы создаете объект, не существующий в том же расширении ядра как Ваш код. Путем передачи размера класса как параметр malloc, C++ компилирует значение размера в двоичный файл вызова. Но если существуют какие-либо зависимости от того размера, и класс позже становится больше, двоичный файл мог бы прервать различные тонкие пути, такой как путем переписывания последующих блоков выделения. OSTypeAlloc макрос, с другой стороны, позволяет классу, делающему выделение определять свой собственный размер.

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

Глобальные инициализаторы

Глобальные конструкторы — иногда называемый глобальными инициализаторами — являются полезной функцией языка C++. Для драйверов они разрешают безопасную инициализацию и выделение ресурсов, которые должны быть доступными, прежде чем драйвер начнет передавать данные. Для надлежащего понимания, что глобальные конструкторы и почему они полезны давайте сначала рассмотрим, когда объекты C++ с различными видами объема создаются и уничтожаются.

Область объекта и вызов конструктора

C++ дает Вам четыре различных способа создать объект. В каждом случае объект имеет определенный объем, потому что вызовы его конструктора и деструктора происходят в определенные времена. Где конструкция объекта происходит в результате объявления (автоматическая локальная, статическая локальная переменная, и глобальный), только конструктор по умолчанию вызывается.

  • Явно созданные объекты Такие объекты динамично создаются через new оператор и уничтожается через delete оператор. Время жизни объекта является периодом между new и delete, когда конструктора и деструктор вызывают, потому что это. OSMetaClass базирует свой механизм для динамического выделения объектов libkern на new оператор. Все объекты, наследовавшиеся от OSObject, могут только быть явно созданными объектами. OSMetaClassBase retain и release вызовы реализуют более сложный механизм подсчета ссылок — на основе new и delete— для объектной персистентности.

  • Автоматические локальные объекты автоматический локальный объект создается каждый контроль времени, проходят через его объявление в функции членства. Его деструктор призван обратно каждый раз передачи управления выше объявления объекта или из блока включения. Следующий пример иллюстрирует объем автоматических локальных объектов (IntArray класс C++):

    void func() {
        IntArray a1;                // a1 constructed here 1 time
        int l = 2;
        for (i=0; i < 3; i++) {
            IntArray a2;            // a2 constructed here 3 times
            if (i == l) {
                IntArray a3;        // a3 constructed here 1 time
                // ...                 (when "if" is true)
            }                       // a3 destroyed at exit of "if" block
        }                           // a2 destroyed here 3 times
    }                               // a1 destroyed here 1 time
  • Статические локальные объекты Эти объекты подобны автоматическим локальным объектам, но с несколькими важными различиями. Статические локальные объекты объявляются в функции членства с a static ключевое слово. Когда управление проходит через объявление, но только в первый раз, как автоматический локальный объект, вызывают конструктора статического объекта. Если управление никогда не пройдет через свое объявление, это не будет создано. Деструктор статического локального объекта вызывают только, когда исполнимая программа обычно выходит (такой как тогда, когда main возвраты или exit вызывается или расширение ядра разгружено).

  • Глобальные объекты глобальный объект объявляются за пределами любой функции. Это существует в течение жизни во время выполнения исполнимой программы. Конструктор глобального объекта вызывается перед точкой входа исполнимой программы; деструктор вызывают после выходов программы. Добавление ключевого слова static к пределам объявления объем объекта к единице перевода (обычно объектный файл), в котором это определяется.

Для драйверов Набора I/O объем глобального объекта влечет за собой гарантию, что две вещи произойдут:

  • Конструктора вызывают перед вызовом функциональной точки входа его KEXT-двоичного-файла. Для драйверов глобальных конструкторов вызывают во время загрузки; для других расширений ядра глобальных конструкторов вызывают прежде MODULE_START.

  • Код, работающий в конструкторе, является однопоточным (на двоичный файл KEXT).

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

Существует несколько протестов отметить об объектах C++ с глобальной областью видимости. Во-первых, такие объекты не могут произойти из OSObject; как отмечено ранее, libkern разрешает только явное создание объектов. Во-вторых, если Ваш код имеет многократные глобальные инициализаторы в той же единице перевода, они вызываются в порядке их определения. Однако, если у Вас есть многократные глобальные инициализаторы в различных двоичных файлах KEXT, порядок их вызова между двоичными файлами не определен. Из-за этого это - хорошая практика программирования для предотвращения зависимостей среди глобальных инициализаторов.

Пример глобального инициализатора

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

Семья I/O Kit Serial использует глобальный инициализатор для инициализации глобальных структур данных. Перечисление 1-1 показывает определение (частного) класса IOSerialBSDClientGlobals. После определения объявление a static переменная класса; это определение генерирует единственный экземпляр класса и говорит компилятору, что этот экземпляр и его данные являются глобальной переменной в объеме.

  Определение перечисления 1-1 класса, который будет объявлен глобальной переменной

class IOSerialBSDClientGlobals {
private:
 
    unsigned int fMajor;
    unsigned int fLastMinor;
    IOSerialBSDClient **fClients;
    OSDictionary *fNames;
 
public:
    IOSerialBSDClientGlobals();
    ~IOSerialBSDClientGlobals();
 
    inline bool isValid();
    inline IOSerialBSDClient *getClient(dev_t dev);
 
    dev_t assign_dev_t();
    bool registerTTY(dev_t dev, IOSerialBSDClient *tty);
    const OSSymbol *getUniqueTTYSuffix
        (const OSSymbol *inName, const OSSymbol *suffix, dev_t dev);
    void releaseUniqueTTYSuffix(const OSSymbol *inName, const OSSymbol *suffix);
};
 
static IOSerialBSDClientGlobals sBSDGlobals;

Объявление sBSDGlobals переменная начинает конструктора для класса IOSerialBSDClientGlobals (и другие глобальные инициализаторы) во время загрузки. Семья Serial реализует этого конструктора как показано в Перечислении 1-2.

  Реализация перечисления 1-2 глобального конструктора

#define OSSYM(str) OSSymbol::withCStringNoCopy(str)
IOSerialBSDClientGlobals::IOSerialBSDClientGlobals()
{
    gIOSerialBSDServiceValue = OSSYM(kIOSerialBSDServiceValue);
    gIOSerialBSDTypeKey      = OSSYM(kIOSerialBSDTypeKey);
    gIOSerialBSDAllTypes     = OSSYM(kIOSerialBSDAllTypes);
    gIOSerialBSDModemType    = OSSYM(kIOSerialBSDModemType);
    gIOSerialBSDRS232Type    = OSSYM(kIOSerialBSDRS232Type);
    gIOTTYDeviceKey          = OSSYM(kIOTTYDeviceKey);
    gIOTTYBaseNameKey        = OSSYM(kIOTTYBaseNameKey);
    gIOTTYSuffixKey          = OSSYM(kIOTTYSuffixKey);
    gIOCalloutDeviceKey      = OSSYM(kIOCalloutDeviceKey);
    gIODialinDeviceKey       = OSSYM(kIODialinDeviceKey);
    gIOTTYWaitForIdleKey     = OSSYM(kIOTTYWaitForIdleKey);
 
    fMajor = (unsigned int) -1;
    fNames = OSDictionary::withCapacity(4);
    fLastMinor = 4;
    fClients = (IOSerialBSDClient **)
                IOMalloc(fLastMinor * sizeof(fClients[0]));
    if (fClients && fNames) {
        bzero(fClients, fLastMinor * sizeof(fClients[0]));
        fMajor = cdevsw_add(-1, &IOSerialBSDClient::devsw);
    }
 
    if (!isValid())
        IOLog("IOSerialBSDClient didn't initialize");
}
#undef OSSYM

Этот код создает объекты OSSymbol для различных атрибутов семьи Serial и выделяет другие глобальные ресурсы. Одной вещью отметить об этом примере кода является вызов к isValid; эта функция членства просто проверяет, успешно выполнилась ли глобальная инициализация. Поскольку исключения исключены из ограниченной формы ядра C++, необходимо осуществить некоторую проверку проверки как это или в конструкторе само или в start функция членства. Если бы глобальный инициализатор не выполнял свою работу, то Ваш драйвер должен корректно выйти.

Деструктор для IOSerialBSDClientGlobals (см. Перечисление 1-3) класс освобождает глобальные ресурсы, создаваемые конструктором. Глобальный деструктор вызывают после вызова MODULE_STOP функция членства или, в случае драйверов, в разгружает время.

  Реализация перечисления 1-3 глобального деструктора

IOSerialBSDClientGlobals::~IOSerialBSDClientGlobals()
{
    SAFE_RELEASE(gIOSerialBSDServiceValue);
    SAFE_RELEASE(gIOSerialBSDTypeKey);
    SAFE_RELEASE(gIOSerialBSDAllTypes);
    SAFE_RELEASE(gIOSerialBSDModemType);
    SAFE_RELEASE(gIOSerialBSDRS232Type);
    SAFE_RELEASE(gIOTTYDeviceKey);
    SAFE_RELEASE(gIOTTYBaseNameKey);
    SAFE_RELEASE(gIOTTYSuffixKey);
    SAFE_RELEASE(gIOCalloutDeviceKey);
    SAFE_RELEASE(gIODialinDeviceKey);
    SAFE_RELEASE(gIOTTYWaitForIdleKey);
    SAFE_RELEASE(fNames);
    if (fMajor != (unsigned int) -1)
        cdevsw_remove(fMajor, &IOSerialBSDClient::devsw);
    if (fClients)
        IOFree(fClients, fLastMinor * sizeof(fClients[0]));
}

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

Объектный самоанализ и динамический кастинг

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

Таблица 1-1 описывает функции самоанализа и макросы.

Таблица 1-1  OSMetaClass и макросы самоанализа OSMetaClassBase и функции членства

Функция членства или макрос

Описание

OSTypeID

Макрос, что, учитывая имя класса (не заключенный в кавычки), возвращает обозначенный экземпляр OSMetaClass.

OSTypeIDInst

Макрос, что, приведенный пример производного класса OSObject, возвращает класс того объекта (как экземпляр OSMetaClass).

OSCheckTypeInst

Макрос, определяющий, наследовался ли данный объект прямо или косвенно от того же класса как ссылочный объект (т.е. объект, тип класса которого известен).

isEqualTo

Возвраты true если два объекта эквивалентны. Какие эквивалентные средние значения зависит от libkern или производного класса Набора I/O, переопределяющего эту функцию членства. OSMetaClassBase реализует эту функцию как мелкое сравнение указателя.

metaCast

Определяет, наследовался ли объект от данного класса; варианты функции членства позволяют Вам указать класс как экземпляр OSMetaClass, объект OSSymbol, объект OSString или струну до. Определенный классом OSMetaClassBase.

checkMetaCastWithName

Подобный metaCast, но реализованный как статическая функция членства OSMetaClass.

getInstanceCount

Средство доступа OSMetaClass, возвращающее текущее число экземпляров для данного класса (указанный как экземпляр OSMetaClass) и все производные классы того класса.

getSuperClass

Функция средства доступа OSMetaClass, возвращающая базовый класс указанного класса.

getClassName

Функция средства доступа OSMetaClass, возвращающая имя указанного класса.

getClassSize

Функция средства доступа OSMetaClass, возвращающая размер указанного класса.

OSMetaClassBase также включает полезный названный макрос OSDynamicCast. Этот макрос делает в основном ту же вещь как стандартный C++ dynamic_cast<ввести *>(объект) оператор: Это преобразовывает тип класса объекта другому, совместимому типу. Но перед OSDynamicCast макрос преобразовывает тип экземпляра, это проверяет, что бросок допустим — т.е. это проверяет, наследовался ли приведенный пример от данного типа. Если это не делает, это возвращает нуль. Если или тип класса или экземпляр, указанный в качестве параметра, являются нулем, это также возвращает нуль. Никакие спецификаторы типа, такой как const, позволяются в параметрах.

Перечисление 1-4 иллюстрирует, как Вы могли бы сделать некоторые тесты самоанализа и динамические броски.

  Код перечисления 1-4, показывающий динамический кастинг и самоанализ

void testIntrospection() {
 
    unsigned long long val = 11;
    int count;
    bool yup;
    OSString *strObj = OSString::withCString("Darth Vader");
    OSNumber *numObj = OSTypeAlloc(OSNumber);
    numObj->init(val, 3);
 
    yup = OSCheckTypeInst(strObj, numObj);
    IOLog("strObj %s the same as numObj\n", (yup ? "is" : "is not"));
 
    count = (OSTypeIDInst(numObj))->getInstanceCount();
    IOLog("There are %d instances of OSNumber\n", count);
 
    if (OSDynamicCast(OSString, strObj)) {
    IOLog("Could cast strObj to OSString");
    } else {
    IOLog("Couldn't cast strObj to OSString");
    }
}

Совместимость на уровне двоичных кодов

Что-то, что долго заполоняло разработчиков библиотеки C++, является проблемой хрупкого базового класса. libkern библиотека и, более в частности, предложение класса OSMetaClass способы избежать опасности для обратной совместимости на уровне двоичных кодов, изложенной хрупкими базовыми классами. Но перед приобретением знаний о том APIs Вы могли бы счесть сводку проблемы хрупкого базового класса полезной.

Проблема хрупкого базового класса

Проблема хрупкого базового класса влияет только на нелистовые классы, и затем только, когда те классы определяются в различном KEXTs. Таким образом, если Вы никогда не предназначите свой libkern или класс Набора I/O, чтобы быть базовым классом, то тогда хрупкие базовые классы не будут проблемой (и у Вас нет потребности читать далее). Но если Ваш класс мог бы быть базовым классом к некоторому другому классу, и Вы позже изменяете свой класс определенными способами, все внешние KEXTs с зависимостями от Вашего класса, вероятно, испытают проблемы, которые заставят их повреждаться.

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

  • Новые виртуальные функции членства

  • Изменения в порядке объявлений виртуальной функции

  • Новые переменные экземпляра (элементы данных)

  • Изменения в типе или размере переменных экземпляра (например, изменяясь a short к a long или увеличение максимального размера массива)

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

  • Размер объекта

  • Смещения к защищенным или общедоступным данным

  • Размер виртуальной (vtable) таблицы Вашего класса, а также смещения в нем

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

Рисунок 1-2  агрегированные данные и vtable структуры скомпилированного класса
The aggregate data and vtable structures of a compiled class

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

Резервирование будущих элементов данных

Для подготовки нелистового класса к любому будущему добавлению элементов данных укажите пустое ExpansionData структура и a reserved указатель на ту структуру. Затем когда Вы добавляете поле к этой структуре, выделяете эти дополнительные данные в функции членства инициализации для класса (обычно init).

Введите строки в Перечисление 1-5 в Ваш заголовочный файл класса в разделе с защищенным объемом (как обозначено).

  Объявления Начальной буквы перечисления 1-5 ExpansionData структура и reserved указатель

protected:
/*! @struct ExpansionData
    @discussion This structure helps to expand the capabilities of
    this class in the future.
    */
    struct ExpansionData { };
 
/*! @var reserved
    Reserved for future use.  (Internal use only)  */
    ExpansionData *reserved;

Позже, когда необходимо добавить новые элементы данных к классу, добавьте поле или поля к ExpansionData структура и определяет символ препроцессора для поля, как ссылается reserved указатель. Перечисление 1-6 показывает, как Вы могли бы сделать это.

Перечисление 1-6  , Добавляющее новое поле к ExpansionData структура

struct ExpansionData { int eBlastIForgot; };
ExpansionData *reserved;
#define fBlastIForgot ((com_acme_driver_MyDriverClass::ExpansionData *)
            com_acme_driver_MyDriverClass::reserved)->eBlastIForgot)

Наконец, во время инициализации, выделите недавно расширенную структуру.

Дополнение виртуальной таблицы

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

  1. Считайте виртуальные функции в своем классе, которые являются просто полипревращенными версиями виртуальной функции базового класса.

  2. Вычтите то значение из общего количества виртуальных функций в Вашем классе.

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

Когда Вы определили, в каком количестве зарезервированных слотов Вы будете нуждаться для будущего расширения, укажите OSMetaClassDeclareReservedUnused макрос в Вашем заголовочном файле класса для каждого слота. Этот макрос берет два параметра: имя класса и индекс слота клавиатуры. Перечисление 1-7 показывает, как класс IOHIDDevice указывает OSMetaClassDeclareReservedUnused макрос.

Перечисление 1-7  начальное дополнение класса виртуальная таблица (заголовочный файл)

public:
    // ...
    OSMetaClassDeclareReservedUnused(IOHIDDevice,  0);
    OSMetaClassDeclareReservedUnused(IOHIDDevice,  1);
    OSMetaClassDeclareReservedUnused(IOHIDDevice,  2);
    OSMetaClassDeclareReservedUnused(IOHIDDevice,  3);
    OSMetaClassDeclareReservedUnused(IOHIDDevice,  4);
    OSMetaClassDeclareReservedUnused(IOHIDDevice,  5);
    OSMetaClassDeclareReservedUnused(IOHIDDevice,  6);
    OSMetaClassDeclareReservedUnused(IOHIDDevice,  7);
    OSMetaClassDeclareReservedUnused(IOHIDDevice,  8);
    OSMetaClassDeclareReservedUnused(IOHIDDevice,  9);
    OSMetaClassDeclareReservedUnused(IOHIDDevice, 10);
    // ...

В Вашем файле реализации класса введите соответствие OSMetaClassDefineReservedUnused макросы. Они макросы также берут имя класса и индекс слота клавиатуры как параметры. Перечисление 1-8 показывает им макросы, поскольку они появляются в IOHIDDevice.cpp файл

Перечисление 1-8  начальное дополнение класса виртуальная таблица (файл реализации)

// at end of function implementations
OSMetaClassDefineReservedUnused(IOHIDDevice,  0);
OSMetaClassDefineReservedUnused(IOHIDDevice,  1);
OSMetaClassDefineReservedUnused(IOHIDDevice,  2);
OSMetaClassDefineReservedUnused(IOHIDDevice,  3);
OSMetaClassDefineReservedUnused(IOHIDDevice,  4);
OSMetaClassDefineReservedUnused(IOHIDDevice,  5);
OSMetaClassDefineReservedUnused(IOHIDDevice,  6);
OSMetaClassDefineReservedUnused(IOHIDDevice,  7);
OSMetaClassDefineReservedUnused(IOHIDDevice,  8);
OSMetaClassDefineReservedUnused(IOHIDDevice,  9);
OSMetaClassDefineReservedUnused(IOHIDDevice, 10);
// ...

Когда Вы впоследствии добавляете виртуальную функцию к своему классу, заменяете OSMetaClassDeclareReservedUnused макрос, имеющий самый низкий индекс в Вашем заголовочном файле класса с OSMetaClassDeclareReservedUsed макрос; обязательно используйте тот же индекс. Для документирования замены имейте макрос, сразу предшествуют объявлению функции. Класс IOHIDDevice делает это как показано в Перечислении 1-9.

Перечисление 1-9  , Корректирующееся слоты клавиатуры при добавлении новых виртуальных функций — заголовочный файл

public:
// ...
    OSMetaClassDeclareReservedUsed(IOHIDDevice,  0);
    virtual IOReturn updateElementValues(IOHIDElementCookie * cookies,
                                        UInt32 cookieCount = 1);
 
protected:
    OSMetaClassDeclareReservedUsed(IOHIDDevice,  1);
    virtual IOReturn postElementValues(IOHIDElementCookie * cookies,
                                        UInt32 cookieCount = 1);
 
public:
    OSMetaClassDeclareReservedUsed(IOHIDDevice,  2);
    virtual OSString * newSerialNumberString() const;
 
    OSMetaClassDeclareReservedUnused(IOHIDDevice,  3);
    OSMetaClassDeclareReservedUnused(IOHIDDevice,  4);
    OSMetaClassDeclareReservedUnused(IOHIDDevice,  5);
    OSMetaClassDeclareReservedUnused(IOHIDDevice,  6);
    OSMetaClassDeclareReservedUnused(IOHIDDevice,  7);
    OSMetaClassDeclareReservedUnused(IOHIDDevice,  8);
    OSMetaClassDeclareReservedUnused(IOHIDDevice,  9);
    OSMetaClassDeclareReservedUnused(IOHIDDevice, 10);
// ...

В файле реализации замените индексированный самым низким образом OSMetaClassDefineReservedUnused макрос с a OSMetaClassDefineReservedUsed макрос для каждой новой виртуальной функции. Снова, для пользы ясности, рассмотрите помещение макроса сразу перед реализацией функции. См. Перечисление 1-10 для примера.

Перечисление 1-10  , Корректирующееся слоты клавиатуры при добавлении новых виртуальных функций — файл реализации

OSMetaClassDefineReservedUsed(IOHIDDevice,  0);
IOReturn IOHIDDevice::updateElementValues(IOHIDElementCookie *cookies,
                                        UInt32 cookieCount) {
    // implementation code...
}
 
OSMetaClassDefineReservedUsed(IOHIDDevice,  1);
IOReturn IOHIDDevice::postElementValues(IOHIDElementCookie * cookies,
                                        UInt32 cookieCount) {
    // implementation code...
}
 
OSMetaClassDefineReservedUsed(IOHIDDevice,  2);
OSString * IOHIDDevice::newSerialNumberString() const
{    // implementation code ...
}
OSMetaClassDefineReservedUnused(IOHIDDevice,  3);
OSMetaClassDefineReservedUnused(IOHIDDevice,  4);
OSMetaClassDefineReservedUnused(IOHIDDevice,  5);
OSMetaClassDefineReservedUnused(IOHIDDevice,  6);
OSMetaClassDefineReservedUnused(IOHIDDevice,  7);
OSMetaClassDefineReservedUnused(IOHIDDevice,  8);
OSMetaClassDefineReservedUnused(IOHIDDevice,  9);
OSMetaClassDefineReservedUnused(IOHIDDevice, 10);
// ...