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


cmake/ReplicatePythonSourceTree.cmake
# 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)


  1. evocatus
    20.09.2019 13:20

    А ещё есть cffi


  1. loony_dev Автор
    20.09.2019 13:28

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


  1. kshnkvn
    20.09.2019 15:11

    Как это работает?

    начать контрибьютить

    на гитхабе

    Но зато
    Одно из issue


    1. loony_dev Автор
      20.09.2019 15:14

      Просто некоторые слова очень плохо звучат в транслите и не встречаются в статьях


  1. Magikan
    20.09.2019 19:10

    а чем автора не устраивает pxd? обёртка над libnoise пишется минут за 20 копипастой из хедеров. да, есть момент для автоматизации в виде преобразования с/с++ хедеров в pxd, но там не такие объемы чтобы тратить на это столько времени. Вы же не собираетесь запилить комбайн покрывающий вообще все что есть в линуксах и немного сверху.


    1. loony_dev Автор
      21.09.2019 09:17

      Для начала, я не говорю что оборачивать через pxd это плохо. В том проекте который я переносил, мне лично, было влом расковыривать кучу хедеров, которые были связанны между собой не самым лучшим образом. Тем более, не сильно хочется заниматься рутинной работой.


  1. ser-mk
    21.09.2019 02:59

    А swig не рассматривали?


    1. loony_dev Автор
      21.09.2019 09:13

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


    1. Graphite
      21.09.2019 09:40

      Я много пользовался SWIG'ом, но на данный момент я бы рекомендовал вместо него использовать CLIF. SWIG мощный и с длинной историей, но у него есть ряд неочевидных нюансов. Очень просто выстрелить себе в ногу в общем.