Сообщений 46    Оценка 186 [+1/-2]         Оценить  
Система Orphus

Программирование под Symbian OS: начало

Автор: Дмитрий Москальчук
Источник: RSDN Magazine #2-2005
Опубликовано: 06.11.2005
Исправлено: 10.12.2016
Версия текста: 1.0
C++ в Symbian
Механизм исключений Symbian
Соглашения об именовании
Стандартные библиотеки C и C++
Важнейшие идиомы
С++ и машинная архитектура
Строки в Symbian.
Статические данные
Заключение

Операционная система Symbian на сегодняшний день является одной из самых популярных операционных систем для смартфонов. Прежде всего это, конечно же, смартфоны и коммуникаторы фирм Nokia и SonyEricsson, но не только. Такие фирмы, как Siemens, Panasonic, FOMA также используют Symbian в своих смартфонах. Тем не менее, информации об этой операционной системе для начинающих довольно мало. Данная статья призвана восполнить этот пробел.

В этой статье будут затронуты вопросы основных отличий процесса программирования под Symbian от программирования под mainstream системы типа MS Windows или Linux. Язык программирования – C++. Везде, где это не оговорено отдельно, будут подразумеваться Symbian OS v6.1, 7.0, 7.0s и 8.0.

В этой статье не будут затронуты вопросы получения, установки и настройки SDK. Об этом много информации в Интернете, могу лишь посоветовать для начала два сайта – http://www.symbian.com и http://www.forum.nokia.com.

Итак, начнем.

C++ в Symbian

Главное, и самое большое отличие C++, используемого в Symbian, от стандартного – не поддерживаются C++ исключения (exceptions) и RTTI (Run Time Type Information). Правда, в новой Symbian v9 обещают сделать поддержку полноценного, стандартного C++. В нем будет и то, и другое. Ну что ж, поживем – увидим. Пока же надо обходиться тем, что есть. А есть механизм Symbian исключений, призваный заменить отсутствующие C++ exceptions.

Прежде всего рассмотрим повнимательнее, как работает механизм исключений в стандартном C++. Как только компилятор встречает оператор генерации исключения (throw), он формирует так называемый «фрэйм исключения». Что это такое? Это код последовательного вызова деструкторов всех локальных объектов, «раскрутки стека» и перехода на нужный обработчик. Нужный обработчик определяется по типу генерируемого исключения. Так как компилятору доступна вся информация о типах, он генерирует команду перехода на фиксированный адрес, который ему уже известен. Это и есть адрес начала обработчика. Сравним два участка кода (стандартный C++ и C++ с Symbian exceptions):

      void foo()
{
  throw 2;
}

int main()
{
  try
  {
    foo();
  }
  catch(int err)
  {
    assert(err == 2);
  }
  catch(...)
  {
    throw;
  }

  return 0;
}

      void foo()
{
  User::Leave(2);
}

int main()
{
  int err;
  TRAP(err, foo() );
  switch(err)
  {
  case KErrNone:
    // Do nothingbreak;
  case 2:
    // Handle exceptionbreak;
  default: User::Leave(err); // Generate new exception
  }

  return 0;
}

В Symbian исключение определяется только целочисленным кодом. Аналогом блока try является макрос TRAP. Первый его параметр – имя переменной, в которую при генерации исключения будет занесен код ошибки, или KErrNone, если исключение сгенерировано не было. Таким образом, аналогом череды catch является switch с проверкой кода ошибки. Ну и аналог throw – User::Leave(). Единственный его параметр – код ошибки.

ПРИМЕЧАНИЕ

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

Макрос TRAP при препроцессинге раскрывается так:

#define TRAP(_r,_s) {TTrap __t;if (__t.Trap(_r)==0){_s;TTrap::UnTrap();}}

TTrap – это закрытый для пользователя класс. Утверждается только, что пользовательские приложения не должны напрямую его использовать. Работает он так же, как функция стандартной библиотеки C setjmp. Метод Trap запоминает состояние аппаратных регистров в некую внутреннюю переменную. Метод UnTrap восстанавливает ее значение в предыдущее. Вызов же User::Leave() аналогичен вызову longjmp из C stdlib.

Внимательный читатель, наверное, сейчас воскликнет: «А как же деструкторы локальных объектов? Кто их вызовет?». Вот здесь вся ответственность ложится на программиста. Каждый поток выполнения (thread) в Symbian имеет связанный с ним CleanupStack. В момент вызова TRAP запоминается текущее положение вершины стека и, если произошло исключение, происходит уничтожение всех объектов из этого стека вплоть до запомненного состояния вершины. Для этого все динамические объекты, которые кладутся в CleanupStack, должны быть производными от класса CBase, имеющего в своем составе виртуальный деструктор, а все объекты, создаваемые в стеке, должны быть производными от RHandleBase (в нем определен виртуальный метод Close() ). Вот пример такого кода:

      void foo()
{
  User::Leave(2);
}

class A : public CBase {};
class B : public RHandleBase {};

int main()
{
  int err;
  TRAP(err, (
    A *a = new (ELeave) A;
    // объект, на который указывает a, будет уничтожен в случае неудачи
    CleanupStack::PushL(a); 
    B b;
    // в случае неудачи у объекта b будет вызван метод Close() 
    CleanupClosePushL(b); 
    foo();
    // Эта строка никогда не выполнится, но тем не менее a будет корректно
    // удален, а b корректно закрыт
    CleanupStack::PopAndDestroy(2);
    // Это эквивалентно следующему:// CleanupStack::Pop(2);// delete a;// b.Close();
    ));
  assert(err == 2);

  return 0;
}

Конечно, постоянное слежение за CleanupStack-ом довольно утомительно. Здесь можно использовать RAII и написать несколько wrapper-ов, облегчающих жизнь. Вот, к примеру, что-то типа стандартного auto_ptr, но без возможности копирования и передачи владения:

symb_ptr

Этих знаний достаточно, чтобы начать пользоваться исключениями в Symbian. Для тех же читателей, которые хотят проникнуть глубже в механизм Symbian exceptions, приведена следующая глава. Если вам неинтересно, как устроен этот механизм изнутри, можете ее пропустить.

Механизм исключений Symbian

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

Эмуляция механизма исключений с помощью setjmp/longjmp
        #include <iostream>
#include <setjmp.h>

jmp_buf jbuf;

void foo()
{
  std::cout << "foo" << std::endl;
  longjmp(jbuf, 1);
  std::cout << "bar" << std::endl;
}

int main()
{
  int ret = setjmp(jbuf);
  switch(ret)
  {
  case 0:
    foo();
    break;
  case 1:
    std::cout << "ret: " << ret << std::endl;
    break;
  default:
    std::cout << "Unknown exception" << std::endl;
  }

  return 0;
}

int setjmp(jmp_buf env);

Принимает единственный аргумент типа jmp_buf, в котором сохраняет состояние аппаратных регистров. Реализация системо-зависимая.

Возвращает 0 после сохранения состояния. Если возврат является результатом вызова longjmp, возвращает аргумент функции longjmp.

void longjmp(jmp_buf env, int arg);

Принимает в качестве аргументов буфер, содержащий сохраненное ранее аппаратное состояние и целочисленную переменную, которую вернет setjmp.

Все прекрасно работает. Выводится строчка «foo», затем происходит смоделированная «генерация исключения», которое перехватывается веткой «case 1». Строка «bar» не выводится вообще – до нее выполнение никогда не дойдет. Если переписать это с помощью макросов, все даже будет выглядеть похоже на try/catch.

Эмуляция механизма исключений с помощью setjmp/longjmp. Использование макросов.
        #include <iostream>
#include <setjmp.h>

jmp_buf jbuf;

#define TRY_BEGIN {int ret = setjmp(jbuf); switch(ret) {case 0:
#define TRY_END break;
#define TRY_FINISH default: std::cout << "Unknown exception" << std::endl;}}

#define CATCH_BEGIN(x) case (x):
#define CATCH_END break;

#define THROW(x) longjmp(jbuf, (x))

void foo()
{
  std::cout << "foo" << std::endl;
  THROW(1);
  std::cout << "bar" << std::endl;
}

int main()
{
  TRY_BEGIN
  {
    foo();
  }
  TRY_END
  CATCH_BEGIN(1)
  {
    std::cout << "ret: " << ret << std::endl;
  }
  CATCH_END
  TRY_FINISH

  return 0;
}

Возникает вопрос: «Но если все работает и так, зачем тогда в C++ были введены исключения?». Ответ прост: в приведенном выше коде не будут вызваны деструкторы локальных объектов. Немного изменим код для проверки этого свойства:

Эмуляция механизма исключений с помощью setjmp/longjmp. Проблема с деструкторами локальных объектов.
        #include <iostream>
#include <setjmp.h>

jmp_buf jbuf;

#define TRY_BEGIN {int ret = setjmp(jbuf); switch(ret) {case 0:
#define TRY_END break;
#define TRY_FINISH default: std::cout << "Unknown exception" << std::endl;}}

#define CATCH_BEGIN(x) case (x):
#define CATCH_END break;

#define THROW(x) longjmp(jbuf, (x))

class A
{
public:
  A() {std::cout << "A::A()" << std::endl;}
  ~A() {std::cout << "A::~A()" << std::endl;}
};

void foo()
{
  std::cout << "foo" << std::endl;
  A a;
  THROW(1);
  std::cout << "bar" << std::endl;
}

int main()
{
  TRY_BEGIN
  {
    foo();
  }
  TRY_END
  CATCH_BEGIN(1)
  {
    std::cout << "ret: " << ret << std::endl;
  }
  CATCH_END
  TRY_FINISH

  return 0;
}

Деструктор локального объекта a, как легко заметить, вызван не будет, а это является нарушением основной концепции исключений C++: «деструкторы локальных объектов вызываются всегда, независимо от способа возврата из функции (с помощью return или в связи с выбросом исключения)».

ПРИМЕЧАНИЕ

Если скомпилировать и запустить этот код в среде Microsoft Visual Studio, можно заметить иное поведение – деструктор A будет вызван. Это – частная инициатива MS CRT library. Если код компилируется с ключом /EHsc (Enable C++ exceptions), при вызове longjmp деструкторы локальных объектов все-таки вызываются, но выдается предупреждение о том, что совмещение setjmp и исключений C++ является непереносимым. Для чистоты эксперимента отключите эту опцию.

Именно для корректного уничтожения локальных объектов в Symbian был введен CleanupStack. Создадим его и мы.

Эмуляция механизма исключений с помощью setjmp/longjmp и cleanup_stack-а.
        #include <iostream>
#include <stack>
#include <setjmp.h>

jmp_buf jbuf;

class cleanup_base
{
public:
  virtual ~cleanup_base() = 0;
};

cleanup_base::~cleanup_base() {}

class cleanup_stack
{
public:
  typedef std::stack<cleanup_base *>::size_type size_type;

  static push(cleanup_base *item)
  {
    m_stack.push(item);
  }

  static pop_and_destroy()
  {
    cleanup_base *item = m_stack.top();
    m_stack.pop();
    delete item;
  }

  static pop()
  {
    m_stack.pop();
  }

  static size_type size() 
{
    return m_stack.size();
}

private:
  static std::stack<cleanup_base *> m_stack;
};

std::stack<cleanup_base *> cleanup_stack::m_stack;

cleanup_stack::size_type cleanup_position = 0;

void unwind()
{
  while(cleanup_stack::size() > cleanup_position)
    cleanup_stack::pop_and_destroy();
}

#define TRY_BEGIN \
{\
  cleanup_position = cleanup_stack::size();\
  int ret = setjmp(jbuf);\
  switch(ret) {case 0:

#define TRY_END break;
#define TRY_FINISH default: std::cout << "Unknown exception" << std::endl;}}

#define CATCH_BEGIN(x) case (x):
#define CATCH_END break;

#define THROW(x) unwind(); longjmp(jbuf, (x))


class A : public cleanup_base
{
public:
  A() {std::cout << "A::A()" << std::endl;}
  ~A() {std::cout << "A::~A()" << std::endl;}
};

void foo()
{
  std::cout << "foo" << std::endl;
  A *a = new A;
  cleanup_stack::push(a);
  THROW(1);
  std::cout << "bar" << std::endl;
  cleanup_stack::pop_and_destroy();
}

int main()
{
  TRY_BEGIN
  {
    foo();
  }
  TRY_END
  CATCH_BEGIN(1)
  {
    std::cout << "ret: " << ret << std::endl;
  }
  CATCH_END
  TRY_FINISH

  return 0;
}

Здесь есть один важный момент – все объекты, которые кладутся в cleanup_stack, должны быть производными от cleanup_base. Именно для этих целей в нем определен виртуальный деструктор.

Ок, все работает. Но как быть с автоматическими (созданными в стеке) объектами? Ответ прост: надо создать вспомогательный proxy-класс, который будет инкапсулировать в себе указатель или ссылку на объект и стратегию его уничтожения (будь то delete, вызов предопределенного метода close или еще что). В таком случае cleanup_stack будет содержать не указатели на cleanup_base, а объекты этого proxy-класса. Реализация такого класса оставляется читателю в качестве упражнения.

В таблице 1 проведем соответствия между нашими конструкциями и конструкциями, используемыми в Symbian.

Наши конструкции Symbian
Блок TRY/CATCH Макрос TRAP
Макрос THROW Функция User::Leave()
cleanup_stack CleanupStack
cleanup_base CBase
Наш proxy-класс TCleanupItem

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

Соглашения об именовании

Если посмотреть на какой-нибудь обычный код для Symbian, можно обратить внимание на множество функций и методов, имена которых заканчиваются на L или LC. Это – принятый стиль именования функций, объясняющий своим названием возможность генерирования ею исключения. Суффикс L означает, что функция может сгенерировать исключение. Суффикс C означает, что возвращаемый функцией объект уже помещен ею в CleanupStack и об этом можно не беспокоиться. Суффикс LC – комбинация двух предыдущих. Вот несколько примеров:

      class A
{
public:
  static A *NewL()
  {
    A *self = A::NewLC();
    CleanupStack::Pop(1);
    return self;
  }

  static A *NewLC()
  {
    A *self = new (ELeave) A;
    CleanupStack::PushL(self);
    self->ConstructL();
    return self;
  }

  void ConstructL()
  {
    // Что-то делаем...
  }

private:
  A() {}
};

Конечно же, никто не может гарантировать, что в каком-нибудь стороннем коде функция, имя которой не заканчивается L, не выбросит исключения. Это уже вопрос доброй воли и аккуратности автора кода. Однако в отношении Symbian API в этом можно быть уверенным.

Кстати, приведенный выше код иллюстрирует один из паттернов, часто используемых в Symbian – конструктор закрытый и не генерирует исключений, реально же конструирование объекта происходит в методе ConstructL уже после того, как объект помещен в CleanupStack. Это очень важный момент – в конструкторе нельзя генерировать исключения, так как объект к этому моменту еще не сконструирован и не помещен в CleanupStack! Вся работа, которая потенциально может закончиться генерацией исключения, должна проводиться в функции ConstructL. Параметр ELeave в выражении new означает «сгенерировать исключение, если не удалось создать объект».

ПРЕДУПРЕЖДЕНИЕ

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

Вот полный список соглашений о наименованиях в Symbian:

Классы и структуры

Большинство имен классов сформировано с использованием префиксов C, T, R или M. Означают они следующее:

Имена переменных

Имена членов классов начинаются с префикса «i», например «iMember». Имена аргументов функций начинаются с префикса «a», например «aControl» или «aIndex». Имена локальных переменных не предваряются префиксами. Имена глобальных переменных принято начинать с прописной буквы.

Функции

Имена функций всегда определяются выполняемым действием. Исключением являются методы «геттеры» («getters»): для методов, возвращающих значение переменной-члена класса, имя функции совпадает с названием переменной, но без «i» в начале. Пример:

        inline RWindow& Window() const { return iWindow; }

Функции, которые могут сгенерировать исключение, заканчиваются суффиксом «L». Функции, создающие новые объекты в динамической памяти и сразу заносящие их в CleanupStack, заканчиваются суффиксом «LC». Методы, перехватывающие владение своим объектом и уничтожающие его перед возвратом, имеют суффикс «LD». Пример:

CEikDialog *dialog = new (ELeave) CBossSettingsDialog;
if(dialog->ExecuteLD(R_BOSS_SETTINGS_DIALOG))
{
  // handle successful settings
}

Макроопределения

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

Перечисления

Пример:

        class TDemo
{
public:
  enum TDemoType
  {
    EDemoMember1, EDemoMember2, EDemoMember3
  };
};

Константы

Имена констант начинаются с префикса «K». Для примера можно взять уже упоминавшуюся KErrNone.

Стандартные библиотеки C и C++

В Symbian есть стандартная библиотека C (несколько урезанная), но нет библиотеки C++. Увы. К счастью, в Symbian v9 она, судя по многочисленным обещаниям, появится.

Что же касается C STDLIB, то в ее использовании есть несколько нюансов. По завершении работы программы нужно вызвать функцию CloseSTDLIB(), объявленную в sys/reent.h. В ином случае возможны проблемы с периодическим падением приложения при выходе.

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

Вот список операций, отсутствующих в Symbian C STDLIB:

С отсутствием C++ Standard Library придется смириться. Взамен предлагается использовать Symbian-контейнеры (CArrayFix, CarrayFixFlat, CArrayVar, CArrayVarFlat), Symbian streams (RStoreReadStream, RStoreWriteStream) и т.д. Правда, некоторое время назад в сети ходила информация о портированном под Symbian STLport, но я его не пробовал, поэтому ничего сказать не могу. Ждем Symbian v9, в которой это все уже есть.

Важнейшие идиомы

С++ и машинная архитектура

В Symbian введено несколько типов фиксированной размерности (таблица 2).

Tint32, TUint32 32-битные знаковые и беззнаковые целочисленные переменные. Представлены 32-битным машинным словом. В ARM-архитектуре все 32-битные машинные слова обязаны быть выровненными на 4-байтную границу. Компилятор всегда гарантирует это.
Tint8, TUint8, TText8 8-битные целочисленные значения. Представлены 8-битным байтом. Специфические требования к выравниванию отсутствуют.
Tint16, TUint16, TText16 16-битные целочисленные значения. Представлены 16-битным машинным «полусловом». Обязаны быть выровненными по 2-байтной границе. Доступ к ним организуется чтением 32-битного слова «по маске». Не рекомендуется использовать, кроме как в случаях критической нехватки памяти или обработки Unicode-текста.
Tint64 64-битное беззнаковое целое. Реализован как класс C++, т.к. ARM архитектура не поддерживает 64-битные вычисления
TReal, TReal64 Числа с плавающей точкой двойной точности. Соответствуют IEEE754 64-bit. Реализованы с использованием эмуляции вычислений с плавающей точкой, т.к. ARM не поддерживает инструкции для работы с подобными числами. Рекомендуется использовать целочисленные вычисления там, где это возможно, и только в крайних случаях прибегать к вычислениям с плавающей точкой.
TReal32 32-битные числа с плавающей точкой. Никакого преимущества перед TReal64 не имеют, не считая вдвое меньшего размера переменной.

Строки в Symbian.

Для обработки строк в Symbian определен целый набор классов, называемых одним словом «Дескрипторы».

Дескрипторы-указатели (Pointer descriptors)

Это два класса: TPtr и TPtrC. TPtrC содержит только длину строки и указатель на ее начало. TPtr содержит также максимальную длину строки, кторая может быть представлена этим дескриптором.


Рисунок 1.

Буферные дескрипторы (Buffer descriptors)

Это также два класса: TBufC и TBuf. Реализованы в виде шаблонов C++ и хранят строки внутри себя, не прибегая к динамическому выделению памяти.


Рисунок 2.

Динамические дескрипторы (Heap descriptor)

Хранят указатель на выделенную в куче область памяти.


Рисунок 3.

Иерархия

Все эти дескрипторы наследуются от одного базового класса TDesC, в котором определены базовые виртуальные методы Ptr(), Length() и т.д.


Рисунок 4.

Имеется два вида этих дескрипторов, различающиеся типом хранимых символов – 8- или 16-битных. Они различаются суффиксом: «8» или «16», соответственно. Версии же по умолчанию хранят текст в виде 8-битных символов, если символ препроцессора _UNICODE не определен, и в 16-битных, если определен.

Статические данные

Symbian OS разработана с учетом того, что она будет работать на read-only носителях. Поэтому DLL, находящиеся в ROM, не могут содержать код, пишущий в ee data segment. Поскольку не существует способа на этапе компиляции узнать, где будет расположена DLL (в ROM или в RAM), делается предположение, что она будет расположена в ROM. Вследствие этого в DLL запрещено иметь глобальные неконстантные объекты и переменные. Все подобные переменные должны быть созданы в стеке или во время выполнения программы в динамической памяти. Если у вас в программе все же есть глобальный неконстантный объект, утилита PETran (запускаемая на конечном этапе сборки проекта), выдаст ошибку и не соберет проект.

В отличие от DLL, в обычных исполняемых файлах (EXE) глобальные неконстантные объекты допустимы.

Заключение

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


Эта статья опубликована в журнале RSDN Magazine #2-2005. Информацию о журнале можно найти здесь
    Сообщений 46    Оценка 186 [+1/-2]         Оценить