Сергей Яковлев
Мой блокнот, мастерская, место где я делюсь своим опытом и мыслями

Быстрый взгляд на FFI в Python

FFI (Foreign Function Interface) — это механизм, который позволяет вызывать функции на других языках программирования из кода на целевом языке. Термин foreign означает, что функции приходят из другого языка и среды. Например, с помощью FFI можно вызвать функцию, написанную на языке C, из кода на языке Python и тут функции написанные на C — Foreign Functions. Эта абстракция особенно полезна в ситуациях, когда вы хотите использовать библиотеку или компонент, написанный на другом языке, в своем проекте на текущем языке программирования. FFI часто используется в языках, не обладающих полным доступом к операционной системе, например, в языке JavaScript для доступа к библиотекам на C++.

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

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

История FFI

Идея FFI (Foreign Function Interface) была придумана в конце 1970-х годов в контексте языка программирования Common Lisp. Но использование FFI стало распространено с появлением языка программирования C, который обладал прямым доступом к операционной системе и мог вызывать функции из динамических библиотек и общих объектных файлов. Данный термин также используется официально в языках Haskell, Python и Perl. Другие языки могут применять другую терминологию: например, Java называет это “JNI” (Java Native Interface), а в ряде языков это называется “language bindings”.

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

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

Поддержка FFI в языках программирования

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

Некоторые языки программирования, имеющие встроенную поддержку FFI:

  • C и C++: эти языки имеют прямой доступ к операционной системе и могут вызывать функции из динамических библиотек и общих объектных файлов.
  • Rust: имеет встроенную поддержку для FFI через функции extern, которые могут вызывать функции на языке C.
  • Swift: имеет возможность вызова функций из динамических библиотек на языке C, а также может использовать Objective-C, как альтернативный способ FFI.

Некоторые языки программирования используют библиотеки сторонних разработчиков для реализации FFI. Например:

  • Python: библиотеки ctypes и cffi обеспечивают возможность вызова функций из динамических библиотек на языке C и других языках.
  • Java: библиотека Java Native Access (JNA) обеспечивает возможность вызова функций из библиотек на языке C.
  • JavaScript: библиотеки ffi и ref обеспечивают возможность вызова функций из динамических библиотек на языке C и C++.

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

Поддержка FFI может отличаться в зависимости от языка программирования и используемой библиотеки FFI. Однако, есть несколько языков, которые известны своей хорошей поддержкой FFI. Наверное самым очевидным примером буду языки C и C++, языки, для которых FFI изначально был разработан. Они обладают высокой производительностью и могут работать непосредственно с операционной системой, что обеспечивает широкие возможности для использования FFI. В свою очередь Rust, не смотря на то, что это относительно новый язык, имеет встроенную поддержку FFI через функции extern, что обеспечивает безопасность и производительность. А Python имеет сразу две библиотеки FFI — ctypes и cffi —, которые обеспечивают простой интерфейс для работы с функциями, написанными на языке C и других языках. Java так же является зыком с хорошей поддержкой FFI, реализуемой библиотекой Java Native Access (JNA). Она обеспечивает простой интерфейс для вызова функций, написанных на языке C и других языках. Ну и конечно же JavaScript. Этот язык имеет библиотеки FFI, такие как ffi и ref, которые обеспечивают возможность вызова функций, написанных на языке C и C++.

Популярные сферы применения

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

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

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

Что нужно знать, при создании расширяемой библиотеки

При создании расширяемой библиотеки, которая будет использоваться через FFI, необходимо учитывать несколько важных моментов.

  • Совместимость с FFI: библиотека должна быть написана на языке, совместимом с FFI. Например, C или C++.
  • Соглашения о вызовах: библиотека должна соответствовать соглашению о вызовах для своей целевой архитектуры, чтобы FFI мог корректно вызывать ее функции. Например, такие требования выставляют Java, а также языки платформы .NET.
  • Экспорт функций: библиотека должна экспортировать функции, которые должны быть доступны через FFI. В большинстве случаев это означает использование директив экспорта при компиляции библиотеки.
  • Параметры функций: функции должны быть написаны с учетом того, что их будут вызывать через FFI. Например, должны использоваться типы данных, которые поддерживаются FFI.
  • Возможности языка: необходимо ограничить возможности языка, сделав его подмножество совместимым с другим языком. Например, язык C++ позволяет объявлять функции, которые могут быть вызваны из языка C, но при этом функции не должны выбрасывать исключений либо принимать параметры по ссылке.
  • Безопасность: при написании библиотеки необходимо учитывать безопасность, чтобы избежать уязвимостей, связанных с использованием FFI.
  • Документация: для удобства использования библиотеки через FFI необходимо предоставить подробную документацию, описывающую функции, их аргументы и возвращаемые значения.

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

FFI в Python

В Python поддержка FFI была добавлена в версии 2.2 в 2001 году с помощью модуля ctypes. Разработчик Мартин ван Лёувен (Martin von Löwis) был ответственным за разработку и внедрение модуля ctypes в Python.

Модуль ctypes был вдохновлен библиотекой ffi языка Ruby, и позволяет вызывать функции, экспортированные из динамических библиотек, написанных на C и C++. ctypes предоставляет простой интерфейс для загрузки динамических библиотек и вызова функций из них, что делает FFI доступным для широкой аудитории разработчиков Python.

С течением времени, появилось множество других библиотек FFI для Python, таких как cffi, которая была добавлена в Python 3.2 в 2011 году. cffi предоставляет более высокоуровневый интерфейс, который обеспечивает безопасность типов данных и позволяет определять функции на Python вместо использования заголовочных файлов на C.

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

Еще одним отличием является то, что cffi предоставляет более высокоуровневый интерфейс для работы с библиотеками на языке C. В частности, cffi автоматически определяет типы данных в C-коде, а также позволяет определять и использовать структуры, объединения и массивы данных, что делает использование библиотек на языке C проще и менее подверженным ошибкам.

Кроме того, cffi обладает более широкими возможностями для работы с C++ кодом и позволяет использовать JIT-компиляцию для вызова функций на языке C.

Сферы применения

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

Наиболее распространенные сферы применения FFI в Python включают:

  • Научные вычисления: многие библиотеки для научных вычислений, такие как NumPy, SciPy и TensorFlow, используют C/C++ код для ускорения вычислений.
  • Игровая индустрия: многие игровые движки написаны на C/C++, поэтому FFI используется для интеграции Python-скриптов с игровым движком.
  • Работа с базами данных: библиотеки для работы с базами данных, такие как SQLAlchemy, могут использовать FFI для вызова функций, написанных на C/C++, для ускорения работы с базами данных.
  • Работа с сетью: библиотеки для работы с сетью, такие как Twisted, могут использовать FFI для вызова функций, написанных на C/C++, для ускорения работы с сетью.
  • Работа с графическими интерфейсами: многие библиотеки для создания графических интерфейсов, такие как PyQt и PyGTK, используют FFI для вызова функций, написанных на C/C++, для ускорения работы с графическими элементами интерфейса.
  • Криптография: библиотеки для криптографических операций, такие как PyCrypto и M2Crypto, могут использовать FFI для вызова функций, написанных на C/C++, для ускорения операций с шифрами и подписями.

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

Примеры

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

Например, предположим, что у нас есть динамическая библиотека mylib.so, которая содержит функцию add, которая складывает два числа и возвращает результат. Для простоты я приведу очень примитивный пример такой библиотеки:

int add(int a, int b);

int add(int a, int b) {
    return a + b;
}

Мы можем вызвать эту функцию из Python с помощью модуля ctypes, следующим образом:

import ctypes

# Загрузка библиотеки
mylib = ctypes.cdll.LoadLibrary('./mylib.so')

# Определение типов аргументов и возвращаемого значения
mylib.add.argtypes = (ctypes.c_int, ctypes.c_int)
mylib.add.restype = ctypes.c_int

# Вызов функции
result = mylib.add(2, 3)

print(result) # 5

В этом примере мы загружаем библиотеку mylib.so и определяем типы аргументов и возвращаемого значения функции add. Затем мы вызываем функцию add с аргументами 2 и 3 и выводим результат, который должен быть равен 5.

А вот пример кода, который использует cffi для вызова функции из библиотеки на языке C:

import cffi

# Определяем интерфейс библиотеки на языке C
ffi = cffi.FFI()
ffi.cdef("""
    int printf(const char *format, ...);
""")

# Загружаем библиотеку на языке C
lib = ffi.dlopen(None)

# Эквивалентно коду на языке C: : char arg[] = "world";
arg = ffi.new("char[]", b"world")

# Вызываем функцию из библиотеки на языке C
lib.printf(b"Hello, %s!\n", arg)

Этот код загружает библиотеку на языке C, которая содержит функцию printf. Затем он использует cffi для вызова функции printf с форматированной строкой "Hello, %s!\n" и аргументом "world".

Обратите внимание, что мы передаем байтовые строки (тип bytes) в функцию printf. Это необходимо, потому что функция printf ожидает строки в кодировке ASCII, которая представляется в Python в виде байтовых строк. Если мы передадим строку в форме строки (тип str), мы получим ошибку TypeError.

Рассмотрим чуть более сложный пример использования FFI в Python. Допустим, у нас есть библиотека на языке C, которая реализует некоторую сложную функцию для обработки данных и выглядит следующим образом:

#include <stdlib.h>

struct Point {
    float x;
    float y;
};

void process_data(struct Point *data, size_t length) {
    // обрабатываем данные...
}

Затем мы можем использовать ctypes для вызова функции process_data из нашего Python-кода. Для этого нам нужно определить типы данных, которые используются в функции на языке C, а затем передать эти данные в функцию через FFI.

Вот пример кода на Python, который использует ctypes для вызова функции process_data из нашей библиотеки на языке C:

import ctypes

# Загружаем библиотеку на языке C
lib = ctypes.CDLL("mylib.so")

# Определяем структуру Point, используемую в функции на языке C
class Point(ctypes.Structure):
    _fields_ = [("x", ctypes.c_float), ("y", ctypes.c_float)]

# Создаем массив структур Point
data = (Point * 10)()

# Заполняем массив данными
for i in range(len(data)):
    data[i].x = i
    data[i].y = i ** 2

# Вызываем функцию process_data из библиотеки на языке C
lib.process_data(data, len(data))

В этом примере мы загружаем библиотеку на языке C с помощью ctypes.CDLL и определяем структуру Point с помощью ctypes.Structure. Затем мы создаем массив структур Point и заполняем его данными. Наконец, мы вызываем функцию process_data из библиотеки на языке C, передавая ей массив данных и его длину.

Обратите внимание, что мы используем ctypes.c_float для определения типа данных float на языке C, а также передаем длину массива данных в качестве аргумента функции process_data.

Разберем еще один пример использования FFI в Python. Например, рассмотрим библиотеку OpenGL, которая предоставляет низкоуровневые функции для рендеринга трехмерных графических изображений. В Python можно использовать библиотеку PyOpenGL, которая является оберткой для библиотеки OpenGL, для создания 3D-изображений и анимации.

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

Для этого можно использовать библиотеку ctypes, которая позволяет вызывать функции из динамических библиотек, написанных на C/C++. Вот пример использования ctypes для вызова функций из библиотеки OpenGL:

import ctypes

# загрузка динамической библиотеки OpenGL
libGL = ctypes.CDLL('libGL.so')

# определение типов аргументов и возвращаемого значения функции glVertex3f
libGL.glVertex3f.argtypes = [ctypes.c_float, ctypes.c_float, ctypes.c_float]
libGL.glVertex3f.restype = None

# вызов функции glVertex3f с аргументами x, y и z
libGL.glVertex3f(0.0, 0.0, 0.0)

Здесь мы загружаем динамическую библиотеку libGL.so, которая содержит функцию glVertex3f для рисования точки в 3D-пространстве. Затем мы определяем типы аргументов и возвращаемого значения функции glVertex3f с помощью атрибутов argtypes и restype. Наконец, мы вызываем функцию glVertex3f с аргументами x, y и z.

Таким образом, с помощью FFI и библиотеки ctypes мы можем использовать функции, написанные на C/C++, для более эффективной работы с графикой в Python.

Производительность

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

def fibonacci(n: int):
    if n < 2:
        return 1
    
    return fibonacci(n - 2) + fibonacci(n - 1)

for _ in range(1000000):
    fibonacci(12)
$ /usr/bin/time nice python fibonacci.py
       29.66 real        29.52 user         0.06 sys

Реализация на Python выполняет вычисление в цикле, общее время работы составило 29.66 секунд. Что с FFI? Напишем нехитрый код библиотеки на C:

int fibonacci(int n);

int fibonacci(int n) {
    if (n < 2) {
        return 1;
    }

    return fibonacci(n - 2) + fibonacci(n - 1);
}

И вызовем эту функцию из Python с помощью модуля ctypes, следующим образом:

import ctypes

# Загрузка библиотеки
C = ctypes.cdll.LoadLibrary('./fibonacci.so')

# Определение типов аргументов и возвращаемого значения
C.fibonacci.argtypes = (ctypes.c_int,)
C.fibonacci.restype = ctypes.c_int

# Вызов функции
for _ in range(1000000):
    C.fibonacci(12)

Результат выполнения:

$ /usr/bin/time nice python fibonacci-ffi.py
        1.09 real         1.01 user         0.01 sys

Весьма неплохо, 1.09 против 29.66 — почти в 29 раз быстрее. Разумеется, какое-то время в Python пришлись на трансляцию кода, но оно не столь значимо в данном примере. Реализацию теста и сравнение скомпилированной Python-версии оставлю в качестве домашнего задания, однако предупреждаю, ничего революционного вы там не увидите, прирост по времени будет не значительный. Что вы можете здесь сделать, чтобы сократить разрыв? Вы ничего не можете сделать. C будет быстрее по многим, многим причинам: строгая типизация (больше информации для оптимизаторов), машинный код (без интерпретатора), отсутствие GIL. Python просто медленный. Так было, есть и, скорее всего, будет.

Когда не стоит применять FFI

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

  • Производительность не является первоочередным требованием: Если требуется обрабатывать небольшие объемы данных или если производительность не является первоочередным требованием, то использование FFI может быть излишним.
  • Сложность кода: Если код на другом языке программирования слишком сложен или труден для понимания, то использование FFI может увеличить сложность приложения и его поддержку.
  • Сложности при разработке: Использование FFI может усложнить процесс разработки приложения, так как необходимо учитывать интерфейсы и различные особенности взаимодействия между языками.
  • Ограниченная переносимость: Если код на другом языке программирования не переносим на разные операционные системы или платформы, то использование FFI может ограничить переносимость приложения.
  • Безопасность: Использование FFI может повлечь за собой риски для безопасности приложения, так как код на другом языке программирования может содержать уязвимости, которые могут быть эксплуатированы.

Применение FFI в Python может быть вредным или бессмысленным в следующих случаях:

  • Если доступна нативная Python-библиотека: если в Python уже существует библиотека, которая решает вашу задачу, то использование FFI может быть нецелесообразным. Вместо этого лучше использовать нативную Python-библиотеку, которая была разработана специально для Python и может быть более удобной в использовании.
  • Если производительность не является проблемой: если вы не работаете с большими объемами данных или сложными вычислениями, то использование FFI может быть избыточным. В этом случае лучше использовать нативный Python-код, который может быть более простым и понятным.
  • Если отсутствует опыт работы с FFI: FFI требует знаний низкоуровневых языков программирования, таких как C/C++. Если вы не имеете опыта работы с этими языками, использование FFI может быть трудным и затратным в плане времени и ресурсов.
  • Если FFI не поддерживается в вашей среде: FFI может не быть поддерживаемым в некоторых средах выполнения, таких как браузер или мобильное устройство. В этом случае использование FFI может быть невозможным или ограниченным, и лучше использовать нативные возможности вашей среды выполнения.
  • Если безопасность является проблемой: использование FFI может представлять риск для безопасности, особенно если используется неизвестная библиотека или вызываются функции из внешнего кода. В этом случае, лучше использовать проверенные и надежные библиотеки, и тщательно проверять вызываемый внешний код на наличие уязвимостей.

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

Ссылки

Вернуться в начало