Недавно на Хабре прочитал статью про очень полезный инструмент, и так как я уже давно искал какой-то проект, чтобы начать контрибьютить, решил посмотреть, что там есть на гитхабе и чем можно помочь. Одно из 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, как же без нее.