Pull to refresh
0
InterSystems
InterSystems IRIS: СУБД, ESB, BI, Healthcare

Caché Native Access — работаем с нативными библиотеками в Caché

Reading time 7 min
Views 5.2K
image
Картинка для привлечения внимания

Как известно, Caché это не только СУБД, но и полноценный язык программирования (Caché ObjectScript). И со стороны СУБД, и со стороны Caché ObjectScript (COS) доступ за пределы Caché богат возможностями (в .Net/Java через .Net/Java Gateway, к реляционным СУБД через SQL Gateway, работа с веб-сервисами). Но если говорить о работе с нативными бинарными библиотеками, то такое взаимодействие реализуется через Caché Callout Gateway, который несколько специфичен. О том как радикально облегчить работу с нативными библиотеками непосредственно из COS можно узнать по катом.


Caché Callout Gateway


Сегодня в Caché для работы с нативным кодом используется Caché Callout Gateway. Под этим названием подразумеваются несколько функций, объединенных под одним названием — $ZF(). Эти функции делятся на две группы:
  • $ZF(-1), $ZF(-2). Первая группа функций позволяет работать с системными командами и консольными программами. Это эффективный инструмент, но его недостаток очевиден — всю функциональность библиотеки сложно реализовать в одной или нескольких программах.
    Пример использования $ZF(-1)
    Создание новой папки в рабочей директории с именем «newfolder»:
    set name = "newfolder"
    set status = $ZF(-1, "mkdir " _ name)
    

  • $ZF(-3), $ZF(-5), $ZF(). Вторая группа функций предоставляет доступ к динамическим и статическим библиотекам. Это уже больше похоже на то, что нам нужно. Но не все так просто: $ZF() работают не с любыми библиотеками, а только с библиотеками специального вида — Callout Libraries. Callout Library отличается от обычной библиотеки наличием в коде специальной таблицы символов ZFEntry, которая содержит некий аналог прототипов экспортирумых функций. Более того, тип аргументов экспортируемых функций строго ограничен — поддерживается только int и несколько видов указателей. То есть, чтобы сделать из произвольной библиотеки Callout Library скорее всего придется писать обертку над всей библиотекой, что не удобно.
    Пример создания Callout Library и вызова функции из нее
    Callout Library, файл test.c:
    #define ZF_DLL
    #include <cdzf.h> // Файл cdzf.h находится в папке Cache/dev/cpp/include 
    
    int
    square(int input, int *output)
    {
      *output = input * input;
      return ZF_SUCCESS; 
    }
    
    ZFBEGIN // Таблица символов
    ZFENTRY("square", "iP", square) // "iP" означает, что в square два аргумента - int и int *
    ZFEND
    

    Компиляция (mingw):
    gcc -mdll -fpic test.c -o test.dll
    
    На Линуксе надо заменить -mdll на -shared.

    Вызов square() из Caché:
    USER> do $ZF(-3, "test.dll", "square", 9)
    81
    



Caché Native Access


Чтобы снять ограничения Callout Gateway и сделать работу с нативныим библиотеками удобной, был создан проект CNA. Название — калька с аналогичного проекта под Java-машину JNA.

Возможности CNA:

  • Можно вызывать функции из любой динамической (разделяемой) библиотеки, бинарно совместимой с С
  • Для вызова функций нужен только код на COS — ничего писать на C или другом компилируемом в машинный код языке не надо
  • Поддержка всех простых типов языка C, size_t и указателей
  • Поддержка структур (и вложенных структур)
  • Поддержка потоков Caché
  • Поддерживаемые платформы: Linux (x86-32/64), Windows (x86-32/64)


Установка


Сначала собираем С часть, компилируется одной командой —
make libffi && make
Под Windows можно использовать для компиляции mingw, либо скачать уже готовые бинарные файлы. Потом импортируем файл cna.xml в любую удобную область:
do $system.OBJ.Load("путь к cna.xml", "c")


Пример работы с CNA


Самая простая нативная библиотека, которая есть на всех системах — стандартная библиотека C. В Windows она обычно находится по адресу C:\Windows\System32\msvcrt.dll, в Linux — /usr/lib/libc.so. Попробуем вызвать какую-нибудь функцию из нее, например strlen, у нее такой прототип:
size_t strlen(const char *);


Class CNA.Strlen Extends %RegisteredObject
{
  ClassMethod Call(libcnaPath As %String, libcPath As %String, string As %String) As %Integer
  {
    set cna = ##class(CNA.CNA).%New(libcnaPath)      // Создает объект типа CNA.CNA
    do cna.LoadLibrary(libcPath)                     // Загружаем libc в CNA

    set pString = cna.ConvertStringToPointer(string) // Конвертируем строку в формат C и сохраняем указатель на начало

    // Вызываем strlen: передаем название функции, тип возвращаемого значения, 
    // список типов аргументов и все аргументы через запятую
    set result = cna.CallFunction("strlen", cna.#SIZET, $lb(cna.#POINTER), pString)

    do cna.FreeLibrary()
    return result
  }
}

В терминале:
USER>w ##class(CNA.Strlen).Call("libcna.dll", "C:\Windows\system32\msvcrt.dll", "hello")
5


Подробности реализации


CNA — это связка библиотеки на C и класса Caché. В основном CNA полагается на libffi. libffi — это библиотека, которая позволяет организовать «низкий уровень» интерфейса внешних функций (FFI). Она помогает забыть о существовании различных соглашений о вызове и вызывать функции во время выполнения, без предоставления их спецификаций во время компиляции. Но для вызова функций из libffi нужен адрес функции, а мы хотели бы вызывать функции только по имени. Чтобы получить адрес функции из какой-либо по имени придется пользоваться платформо-зависимыми интерфейсами: POSIX и WinAPI. В POSIX есть механизм dlopen() / dlsym() для загрузки библиотеки и поиска адреса функции, в WinAPI — функции LoadLibrary() и GetProcAddress(). Это одно из препятствий к портированию CNA на другие платформы, хотя с другой стороны, почти все современные системы хоть частично, но поддерживают стандарт POSIX (кроме, разумеется, Windows).

libffi написана на C и ассемблере. Следовательно libffi и есть native library, и доступ к ней из Caché, можно получить только c помощью Callout Gateway. То есть нужно написать прослойку, которая бы соединяла libffi и Caché и являлась бы Callout Library, чтобы к ней можно было обращаться из COS. Примерная схема работы CNA:


На этом этапе появляется проблема преобразования данных. Когда мы вызываем функцию из COS, мы передаем аргументы во внутреннем формате Caché. Нужно передать их в Сallout Gateway, потом в libffi, но при этом еще нужно где-то преобразовать их в формат C. Но Callout Gateway поддерживает очень мало типов данных и если бы мы преобразовывали данные на стороне C, то нам пришлось бы передавать все в виде строк, а потом их парсить, что не удобно по многим причинам. Поэтому было принято решение преобразовывать данные на стороне Cache и передавать все аргументы в виде строк с бинарными данными уже в формате C.

Так как все типы данных С, кроме композитных, это числа, то фактически задача преобразования данных сводится к преобразованию чисел в бинарные строки с помощью COS. Для этих целей в Caché есть замечательные функции, позоволяющие обойти необходимость прямого доступа к данным: $CHAR и $ASCII, преобразовывающие 8-битное число в символ и обратно. Аналоги есть и для всех необходимых чисел — для 16, 32 и 64-битных целых и чисел с плавающей запятой двойной точности. Но есть одно но — все эти функции работают только либо для знаковых, либо для беззнаковых чисел (разумеется, при работе с целыми). В C же, как известно, число любого размера может быть как знаковое, так и беззнаковое. Дополнять эти функции до полноценной работы придется вручную.

Для представления знаковых чисел в C используется дополнительный код:
  • Первый бит отвечает за знак числа: 0 — плюс, 1 — минус
  • Положительные числа кодируются аналогично беззнаковым
  • Максимальное положительное число — 2k-1-1, k — количество бит
  • Код отрицательного числа x совпадает с кодом беззнакового числа 2k+x

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

Рассмотрим пример преобразования для беззнаковых 32-битных чисел. Если число положительное, то просто используем функцию $ZLCHAR, если отрицательное — то надо найти такое беззнаковое число, чтобы в бинарном виде они совпадали. Как искать это число, напрямую следует из определения дополнительного кода — нужно прибавить исходное число к минимальному, которое не помещается в 32 бита — 232 или FFFFFFFF16 + 1. В итоге получается такой код:

if (x < 0) {
    set x = $ZLCHAR($ZHEX("FFFFFFFF") + x + 1)
} else
    set x = $ZLCHAR(x)
}

Следующая проблема — преобразование структур, композитного типа языка C. Все было бы просто, если бы структуры в памяти представлялись таким же образом, каким они записывались — все поля следуют подряд, одно за другим. Но в памяти структура располагается так, чтобы адрес каждого из полей был кратен специальному числу, выравниванию поля. Конец структуры тоже выравнивается — по наибольшему выравниванию поля. Выравнивание нужно из-за того, что большинство платформ либо не умеют работать с невыровненными данными, либо делают это довольно медленно. Обычно на x86 выравнивание равно размеру поля, но есть исключение — 32-битный Linux, там выравнивание всех полей, размер которых больше 4 байт, равно как раз 4 байтам. Подробнее про выравнивание данных можно почитать в этой статье.

Возьмем, как пример, такую структуру:
struct X {
    char a, b; // sizeof(char) == 1
    double c;  // sizeof(double) == 8
    char d;    
};

На x86-32 она в разных ОС будет располагаться в памяти по разному:

На практике такое представление структуры формируется достаточно просто. Нужно последовательно записывать поля в память, но каждой раз формировать отступ(padding) — пустое пространство перед записью. Отступ высчитывается таким образом:
set padding = (alignment - (offset # alignment)) # alignment //offset - адрес конца последней записи


Что пока не работает


1) Целые числа в Caché представляются таким образом, что точная работа с ними гарантируется только пока число не выходит за пределы 64-битного знакового числа. Но в C есть 64-битный беззнаковый тип (unsigned long long). То есть передать во внешнюю функцию число, которое превышает максимальное 64-битное знаковое, 263-1(~ 9 * 1018), не удастся.

2) Для работы с вещественными числами в Caché есть два типа: собственный десятичный и числа с плавающей запятой двойной точности стандарта IEEE 754. То есть аналогов типов языка C float и long double в Caché нет. Работать в CNA c этими типами можно, но при каждом попадании в Caché они будут конвертироваться в double.

3) При работе на Windows с long double скорее всего все будет работать неправильно. Это вызвано тем, что у Microsoft и команды разработчиков mingw принципиально разные взгляды на то, каким должен быть long double. Microsoft считает что и на 32, и на 64-битной системе размер long double — 8 байт. В mingw же на 32 битах — 12 байт, на 64 — 16. И так как CNA компилируется именно с помощью mingw — про long double лучше забыть.

4) Отсутствует поддержка объединений (unions) и битовых полей в структурах (bitfields). Это вызвано тем, что libffi их не поддерживает.

Критика, замечания, предложения — приветствуются.

Весь исходный код выложен на гитхаб под лицензией MIT.
github.com/intersystems-ru/cna
Tags:
Hubs:
+12
Comments 35
Comments Comments 35

Articles

Information

Website
www.intersystems.com
Registered
Founded
1978
Employees
1,001–5,000 employees
Location
США