Недавно на Хабре прочитал статью про очень полезный инструмент, и так как я уже давно искал какой-то проект, чтобы начать контрибьютить, решил посмотреть, что там есть на гитхабе и чем можно помочь. Одно из issue было на счет создания обертки (дальше буду использовать wrapper) для Cи-шной библиотеки. В тот момент я подумал "О, что-то интересное, уверен, это займет не больше часа". Как же сильно я ошибался.
В этой статье я решил показать не один путь для решения подобной задачи, а несколько разных вариантов. Я покажу варианты создания модулей на Pythonс компиляцией в С, использование маленькой самописной библиотеки С в Python и – последний вариант – использование большой C библиотеки в Python без боли и pxd файлов.
Cython
Об этом уже написаны книги, есть много статей, и на Хабре в том числе, так что я не буду сильно заострять внимание на установке или каких-то базовых вещах. Почитать подробней можно здесь
С помощью Cython мы можем решить несколько задач. Для каких-то вкраплений С кода в питон он вообще подходит идеально и частично решает проблему с импортами библиотек.
Давайте рассмотрим простой пример из официальной документации.
from __future__ import print_function
def fib(n):
"""Print the Fibonacci series up to n."""
a, b = 0, 1
while b < n:
print(b, end=' ')
a, b = b, a + b
print()
Сохраним этот файл как fib.pyx
.
.pyx — это специальный формат Cython файлов, который аналогичен .c для С кода и содержит какой-то функционал. Так же есть .pxd, в С это .h и содержит описание функций, структур и т.д.
Для того, чтобы как-то взаимодействовать с функцией fib, нам нужно "скомпилировать" код. Для этого создадим setup.py
с таким наполнением.
from distutils.core import setup
from Cython.Build import cythonize
setup(
ext_modules=cythonize("fib.pyx"),
)
После выполнения команды python3 setup.py build_ext --inplace
вы сможете импортировать это в обычном питоне и наслаждаться скоростью работы как в нормальных компилируемых языках.
import fib
fib.fib(2000)
Но здесь мы писали Python код и превращали его в С, а что насчет написать С код и запустить его в Python?
Не проблема. Создаем новую папку, внутри создаем папку lib
в которой создадим lib/include
и lib/src
, собственно, все кто работал с С, уже знают, что там будет. В основной папке создадим еще папку python_wrap
.
Перейдем в lib/include
и создадим struct.h
, в котором опишем одну функцию и посмотрим, как работать со структурами в С через Cython.
typedef struct struct_test{
int a;
int b;
} struct_test;
int minus(struct_test a);
Создадим еще один файл, который мы назовем include.h
, в нем будет еще одна функция и импорт структуры из struct.h
#include "struct.h"
int sum(struct_test param_in_struct);
Теперь опишем эти функции в файле lib/src/test_main.c
#include "include.h"
int sum(struct_test param_in_struct){
return param_in_struct.a+param_in_struct.b;
}
int minus(struct_test param_in_struct){
return param_in_struct.a-param_in_struct.b;
}
Да, на оригинальность имен переменных я не претендую, но мы почти закончили Си-шную часть. Что еще? Добавить Makefile, точнее CMake. В папке lib
создаем CMakeLists.txt
.
set (TARGET "mal")
include_directories(
include
src
)
set (SOURCES
./src/test_main.c
)
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS}")
add_library(${TARGET} SHARED ${SOURCES})
target_link_libraries(${TARGET} ${LINKLIBS})
add_library(${TARGET}static STATIC ${SOURCES})
target_link_libraries(${TARGET}static ${LINKLIBS})
С основной директории нам нужно указать, что у нас есть проект для компиляции в папке lib
. Создаем еще один файл CMakeLists.txt
, но уже в корне.
cmake_minimum_required(VERSION 2.8.2 FATAL_ERROR)
cmake_policy(VERSION 2.8)
project( TEST )
set (CMAKE_C_FLAGS "-Werror -Wall -Wextra -Wno-unused-parameter -D_GNU_SOURCE -std=c11 -O3 -g ${CMAKE_C_FLAGS}")
add_custom_target( ReplicatePythonSourceTree ALL ${CMAKE_COMMAND} -P
${CMAKE_CURRENT_SOURCE_DIR}/cmake/ReplicatePythonSourceTree.cmake
${CMAKE_CURRENT_BINARY_DIR}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} )
include( GNUInstallDirs )
add_subdirectory(lib)
Здесь я использую еще маленький файл, который переносит структуру Python wrapper в build директорию, чтобы можно было скомпилировать Сython файлы. Этого можно и не делать, если передавать относительные пути на include
директорию и место, где будет библиотека. К примеру, если библиотека уже скомпилированная и установлена в системе, то мы будем задавать пути в системные директории, но об этом позже.
# Note: when executed in the build dir, then CMAKE_CURRENT_SOURCE_DIR is the
# build dir.
file( COPY setup.py DESTINATION "${CMAKE_ARGV3}"
FILES_MATCHING PATTERN "*.py" )
file( COPY lib/src lib/include DESTINATION "${CMAKE_ARGV3}")
file(GLOB MY_WRAP
"python_wrap/*"
)
file( COPY ${MY_WRAP} DESTINATION "${CMAKE_ARGV3}")
Перед тем, как собирать наш проект, займемся Python частью. В папке python_wrap
создаем два файла main.pxd
и main.pyx
. В main.pxd
нам нужно описать то, что у нас есть в *.h
файлах.
cdef extern from "include/include.h":
ctypedef struct struct_test:
int a
int b
int sum(struct_test param_in_struct);
int minus(struct_test param_in_struct);
С помощью cdef extern from "include/include.h"
указываем, какой файл мы будем описывать. Дальше идет ctypedef struct struct_test:
описание структуры, для того, чтобы ее можно было использовать из Python кода. В конце, собственно, описание двух функций. Хочу заметить, что нам нужно описывать все include, которые есть в include.h
, если он импортирует структуру и функцию из другого header файла, мы считаем, что все это находиться в одном файле.
В main.pyx
запишем функции перехода от Python к C. Это не обязательно, но зачем нагружать Python код структурами для C. Для создания структуры достаточно определить словарь со всеми параметрами.
from main cimport sum, minus
def sum_py(int x, int y):
return sum({"a":x,"b":y})
def minus_py(int x, int y):
return minus({"a":x,"b":y})
Теперь нам нужно сделать так, чтобы это все собиралось вместе. Добавим в корень проекта файл setup.py
, как мы делали уже раньше.
from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext
ext_modules = [Extension('main',
['main.pyx'],
libraries=['mal'],
library_dirs=['lib/'])]
setup(name = 'work extension module',
cmdclass = {'build_ext': build_ext},
ext_modules = ext_modules)
Для того, чтобы скомпилировать C код и собрать нашу библиотеку, создадим простенький bash скрипт.
#!/bin/sh
rm -rf build; mkdir build && cd build
cmake .. && make $@
python3 setup.py build_ext -i
Запускаем и проверяем
$ sh build.sh
$ python3
> import build.main as main
> dir(main)
[.... 'minus_py', 'sum_py']
> main.minus_py(10,2)
8
> main.sum_py(10,2)
12
Ctypesgen
Прошлый пример был очень простой и понятный, но что если вам нужно обернуть очень большую библиотеку, писать все .pxd файлы руками очень долго и трудно, так что, появляется резонный вопрос, что можно использовать, чтобы автоматизировать процесс?
Клонируем репозиторий git clone https://github.com/davidjamesca/ctypesgen.git
. Переходим в ранее собранную библиотеку build/lib/
и запускаем скрипт.
python3 ~/ctypesgen/run.py -lmal ../include/*.h -o main_wrap.py
После этого проверяем работу.
$ python3
> import main_wrap as main
> dir(main)
[... 'struct_test', 'minus', 'sum']
> main.sum(main.struct_struct_test(1,2))
3
> main.minus(main.struct_struct_test(1,2))
-1
Ну и возвращаясь к вопросу о уже установленных библиотеках, допустим, что мы хотим сделать wrapper на neon библиотеку (которая уже установлена в системе любым удобным способом), как показано в Readme Сtypesgen.
$ ctypesgen.py -lneon /usr/local/include/neon/ne_*.h -o neon.py
$ python
> import neon
> dir(neon)
[...,'sys', 'time_t', 'union_ne_session_status_info_u', 'wstring_at']
Напоследок, ссылка на github, как же без нее.
Комментарии (9)
loony_dev Автор
20.09.2019 13:28Спасибо, тоже полезно. По доке выглядит круто, может дополню статью чуть позже.
Magikan
20.09.2019 19:10а чем автора не устраивает pxd? обёртка над libnoise пишется минут за 20 копипастой из хедеров. да, есть момент для автоматизации в виде преобразования с/с++ хедеров в pxd, но там не такие объемы чтобы тратить на это столько времени. Вы же не собираетесь запилить комбайн покрывающий вообще все что есть в линуксах и немного сверху.
loony_dev Автор
21.09.2019 09:17Для начала, я не говорю что оборачивать через pxd это плохо. В том проекте который я переносил, мне лично, было влом расковыривать кучу хедеров, которые были связанны между собой не самым лучшим образом. Тем более, не сильно хочется заниматься рутинной работой.
ser-mk
21.09.2019 02:59А swig не рассматривали?
loony_dev Автор
21.09.2019 09:13Смотрится тоже интересно, ссылки на него у меня уже отмечены открытыми, но я не помню почему его не использовал. Тем не менее, выше еще скидывали инструмент, возможно стоит собрать это во вторую часть.
Graphite
21.09.2019 09:40Я много пользовался SWIG'ом, но на данный момент я бы рекомендовал вместо него использовать CLIF. SWIG мощный и с длинной историей, но у него есть ряд неочевидных нюансов. Очень просто выстрелить себе в ногу в общем.
evocatus
А ещё есть cffi