Все статьи FFI - механизм интеграции между языками программирования
FFI (Foreign function interface) — механизм, с помощью которого код, написанный на одном языке программирования, может вызывать подпрограммы или использовать утилитные средства из кода другом языке программирования.
Термин FFI возник в языке программирования Common Lisp, где есть явная спецификация способов взаимодействия кода с другими языками. Данный термин также используется официально в языках Haskell, Python и Perl. Другие языки могут применять другую терминологию: например, Java называет это “JNI” (Java Native Interface), а в ряде языков это называется “language bindings”.
Основная задача FFI — совместить семантику и соглашения о вызове процедур и функций в двух различных языках. Для реализации потребуется учесть особенности райнтайма и ABI в обоих языках. Есть несколько способов реализовать FFI:
- Требовать, чтобы функции, которые можно вызвать из другого языка, были объявлены определённым образом. Например, такие требования выставляют Java, а также языки платформы .NET.
- Ограничить возможности языка, сделав его подмножество совместимым с другим языком. Например, язык C++ позволяет объявлять функции, которые могут быть вызваны из языка C, но при этом функции не должны выбрасывать исключений либо принимать параметры по ссылке.
- Использовать генератор кода, такой как SWIG, который автоматически просканирует программный модуль на одном языке и сгенерирует библиотечную обёртку для его вызова из другого языка.
Механизм FFI может столкнуться с ограничениями, а именно:
- Если один из языков использует сборку мусора, потребуется удерживать ссылки на объекты из второго языка, чтобы они не были внезапно удалены
- Сложные типы данных, такие как словари или множества, следует отобразить в соответствующие типы в другом языке, что бывает затруднительно и мешает, например, одновременно работать с одним и тем же изменяемым объектом из двух языков
- Как минимум один из языков может работать в окружении виртуальной машины или интерпретатора
- Межязыковое наследование классов и отображение других особенностей системы типов и моделей композиции объектов может быть неоднозначным или неполным
Роль языка C89 / C99 в построении FFI
Язык C в диалектах 89-го или 99-го годов чаще всего используется как цель для FFI по ряду причин:
- в языке C нет своей системы сложных типов, таких как строка или ассоциативный массив, и нет виртуальной машины и автоматического управления памятью, что намного упрощает отображение типов
- язык C является кроссплатформенным аналогом ассемблера и напрямую может сделать всё, что может сделать ассемблер, что позволяет проектировать FFI достаточно вольно
FFI между C++ и C
C++ вобрал в себя язык C как подмножество, и поэтому механизм FFI между ними тривиален: обычно хватает простого объявления функции с extern "C"
:
#ifdef __cplusplus // Если это компилятор C++, явно объявляем линковку в стиле C
extern "C" {
#endif
void PrintLn(const char *line);
void SumAllArguments(const unsigned nArgsCount, ...);
#ifdef __cplusplus // Если это компилятор C++, закрываем область линковки в стиле C
}
#endif
Объявление
extern "C"
приводит к отключению механизма кодирования имён функций, который позволяет языку C++ реализовать классы, пространства имён и перегрузку функций, при этом сохраняя совместимость с языком C. Например, при генерации кода некоторые компиляторы заменяют имя функцииformat
с полной сигнатуройstd::string article::format(void)
на_ZN7article6formatE
. Разные компиляторы на разных платформах могут иметь различные алгоритмы кодирования имён.
Функция на языке C++, вызываемая из языка C, не должна выбрасывать исключений, а также использовать в параметрах ссылки либо составные типы, не должна находиться в пространстве имён или в определении класса. Организовать отображение класса из языка C++ в язык C можно таким образом:
#ifdef __cplusplus
extern "C" {
#endif
// Поля структуры TextScanner не раскрываются.
struct TextScanner;
// В языке C тип структуры S имеет полное имя `struct S`
// поэтому для удобства используют трюк с typedef.
typedef stuct TextScanner TextScanner;
TextScanner *CreateScanner(unsigned options); // отображение конструктора
void DisposeScanner(TextScanner *scanner); // отображение деструктора
void Scanner_ScanFile(TextScanner *scanner, const char *pathUtf8);
void Scanner_ScanString(TextScanner *scanner, const char *textUtf8);
unsigned Scanner_GetWordsCount(TextScanner *scanner);
unsigned Scanner_GetParagraphsCount(TextScanner *scanner);
#ifdef __cplusplus
}
#endif
В языке C нет универсального механизма обработки ошибок, и у вас есть несколько вариантов:
- Передавать код ошибки (целое число или enum) в возвращаемом функцией значении
- Накапливать подробную информацию об ошибке в собственной глобальной либо thread local переменной (так поступает стандартная библиотека C, в которой есть переменная errno и вызов strerror, сходным образом работает OpenGL)
- Предложить установить колбек, который будет вызываться при каждом появлении ошибки.
При приёме колбеков из языка C надо помнить о трюке с передачей контекста через void*
. По примеру ниже нетрудно догадаться, что пользователь может передать через void*
указатель на объект, структуру данных или любые другие контекстные данные:
// Объявляем указатель на функцию с именем TIMER_PROCEDURE
// В параметре void* колбек получает произвольный указатель,
// ранее переданный в SetTimerCallback
typedef void (*TIMER_PROCEDURE)(void *context);
// Вызывает указанную процедуру через intervalMS миллисекунд,
// передаёт ей пользователький параметр context
void SetTimerCallback(unsigned intervalMS, TIMER_PROCEDURE procedure, void *context)
{
// Тут мы как-то встраиваем в цикл событий запись о вызове процедуры таймера
g_timers.Add(intervalMS, [=]() {
procedure(context);
});
}
FFI между Python и C
Язык Python имеет самодостаточный интерпретатор со множеством готовых компонентов. Расширение возможностей языка Python реализуется подключением псевдо-пакетов, таких как future
или ctype
. Более того, многие из стандартных модулей Python, а также основная реализация интерпретатора этого языка написаны на C и C++.
Модуль ctypes предоставляет простой интерфейс для взаимодействия с кодом, скомпилированным из C в виде динамической библиотеки. На Windows следующий код загрузит стандартную библиотеку C как объект, методы которого эквивалентны вызовам функций библиотеки C:
from ctypes import *
print cdll.msvcrt # Prints `<CDLL 'msvcrt', handle ... at ...>`
libc = cdll.msvcrt
libc.printf("%d\n", 42)
На Linux библиотеки приходится загружать явно с указанием как минимум имени файла, и эквивалентом будет следующий код:
from ctypes import *
# Либо вызываем LoadLibrary
cdll.LoadLibrary("libc.so.6")
# Либо конструируем объект типа CDLL
libc = CDLL("libc.so.6")
libc.printf("%d\n", 42)
Иногда имена функций в разделяемой библиотеке не являются правильными идентификаторами Python, например, из-за кодирования имён функций в C++. Получить к ним доступ можно с помощью getattr:
getattr(cdll.msvcrt, "??2@YAPAXI@Z")
В модуле ctype действует ряд правил отображение типов:
None
отображается вNULL
- целые числа форматов
int
иlong
из Python в C отображаются вint
- байтовые и юникодные строки отображаются в завершённый нулевым символом указатель на
char*
иwchar_t
соответственно
Объявлен также ряд типов, эквивалентных базовым типам языка C:
text = c_char_p("Hello, World")
count = c_long(333)
for i in xrange(0, count):
libc.puts(text)
Упражнения
Напишите на языке C++ модуль, решающий квадратные либо кубические уравнения, с адекватной обработкой ошибочных ситуаций (из разряда “уравнение не является квадратным”) и различного числа корней. Внешний интерфейс данного модуля сделайте доступным в виде функций и констант языка C.
После этого напишите консольную программу, которая позволяет пользователю решать квадратные уравнения, используя написанный модуль через FFI. Вы можете выбрать один из языков, перечисленных ниже, либо согласовать с преподавателем свой вариант. При выполнении вы можете писать дополнительный код на языке C, если того требует выбранный вами механизм FFI.
- Язык Python с модулем ctypes
import ctypes
libc = ctypes.CDLL('/lib/libc.so.6') # under Linux/Unix
time = libc.time(None) # equivalent C code: t = time(NULL)
print time
- Язык Javascript на Node.js с использованием node-ffi
var ffi = require('ffi');
var libm = ffi.Library('libm', {
'ceil': [ 'double', [ 'double' ] ]
});
libm.ceil(1.5); // 2
JNIEXPORT void JNICALL Java_ClassName_MethodName
(JNIEnv *env, jobject obj)
{
/*Implement Native Method Here*/
}
- Язык C# с использованием P/Invoke
[DllImport("shell32.dll")]
static extern IntPtr ExtractIcon(
IntPtr hInst,
[MarshalAs(UnmanagedType.LPStr)] string lpszExeFileName,
uint nIconIndex);
- Язык Go с использованием CGO
package print
// #include <stdio.h>
// #include <stdlib.h>
import "C"
import "unsafe"
func Print(s string) {
cs := C.CString(s)
C.fputs(cs, (*C.FILE)(C.stdout))
C.free(unsafe.Pointer(cs))
}
- Язык PASCAL с использованием H2PAS