Тонкости использования языка Python: Часть 6. Способы интеграции Python и С/С++ приложений

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

Олег Цилюрик, преподаватель тренингового отделения, Global Logic

Фото автораОлег Иванович Цилюрик, много лет был разработчиком программного обеспечения в крупных центрах разработки: ВНИИ РТ, НПО "Дельта", КБ ПМ. Последние годы работал над проектами в области промышленной автоматики, IP телефонии и коммуникаций. Автор нескольких книг. Преподаватель тренингового отделения международной софтверной компании Global Logic.



18.12.2013

Введение

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

Тем не менее, стоит отметить, что многие из модулей, известные в Python как стандартные, на самом деле реализованы на языке C. Поэтому возникает интересная возможность реализовывать собственные целевые модули Python на C или C++. Хотя мотивы для подобного решения могут быть самыми разными, но стоит перечислить несколько ключевых:

  • эффективность реализации ресурсоёмких алгоритмов и вычислительных процедур на C, так как за счёт компилирующей природы этого языка скорость выполнения таких реализаций может быть в 40 (а в отдельных случаях и в 100) раз быстрее, чем на Python;
  • предоставить Python-приложениям доступ к различным аппаратно или системно-зависимым сущностям (аудио, аппаратно-зависимые счётчики и т.д.);
  • простой способ реализовать интерфейс к множеству целевых библиотек открытых проектов на C для использования их из Python-приложений;

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


Альтернативы

Существуют (разрабатываются или находятся в разной степени готовности к эксплуатации) несколько альтернативных способов реализации интерфейса из C в Python. Каждый из них имеет свои особенности, преимущества и недостатки, связанные с конкретными требованиями к ситуации. Ниже мы перечислим только некоторые из них, которые будут подробно рассмотрены в этой и последующей статьях:

  • модуль ctypes из стандартной библиотеки модулей Python (например, /usr/lib/python2.7/ctypes/* - версия 2.7 здесь и далее показана совершенно условно, у вас она может отличаться);
  • ручное написание интерфейса модуля;
  • пакет distutils из стандартной библиотеки Python;
  • библиотека Boost.Python;
  • проект Cython;
  • проект SWIG;

В случае применения любого инструмента, конечный результат будет выглядеть как динамически связываемая библиотека (DLL — .so в Linux, .dll в Windows), функции которой можно будет вызывать из кода Python.


Модуль ctypes

Модуль ctypes присутствует в стандартной поставке Python. Это самый простой способ с точки зрения написания кода и сборки, не требующий никаких дополнительных инструментов. Но это и самый опасный способ, так как на стыке Python/C отсутствует не только какой-либо контроль правильности соответствия типов ожидаемых параметров, но даже просто элементарный контроль их количества!

Для примера создадим несколько произвольных С-функций с самыми разнообразными наборами предполагаемых параметров и возвращаемых значений (см. файл call.c в архиве python_c_interaction.tgz в разделе "Материалы для скачивания"):

Листинг 1. Тестовые функции C
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <locale.h>
#include <wchar.h>

// c_double fun1( c_short )
double fun1( short i ) {
   printf( "получен параметр = %d\n", i );
   return (double)i;
}

// c_void_p fun2( c_char_p )
const char* fun2( const char* s ) {
   printf( "локализация операционной системы: %s\n",
           setlocale( LC_ALL, "" ) );      // по умолчанию, из установок системы
   int nsym = 0;
   const char *p = s;
   while( 1 ) {
      int n = mblen( p, MB_CUR_MAX );
      if( 0 == n ) break;
      nsym++;
      p += n;
   };
   printf( "получен параметр [ mbchar* ] :
           \"%s\"; длина: в байтах = %d, в символах = %d\n", s, strlen( s ), nsym );
   static const char* ret = "возвращаемая строка";
   return ret;
}

// c_void_p fun3( c_float, c_long, c_char_p )
void* fun3( float f, long l, char* s ) {   
   printf( "вызов функции 3-х переменных: \"float\"=%f, \"long\"=%ld,
            \"char*\"=\"%s\"\n", f, l, s );
   return (void*)s;
}

Выполним сборку динамической библиотеки следующей командой:

$ gcc -shared -o call.so -fPIC call.c

Для демонстрации вызовов подготовленных функций из кода Python подготовим тестовую программу, представленную в листинге 2 (см. файл test.py из архива python_c_interaction.tgz):

Листинг 2. Тестирование вызовов C-функций
#!/usr/bin/python
# -*- coding: utf-8 -*-
import ctypes

lib = ctypes.CDLL( './call.so' )

print '-----------------------------------------------------'
# === double fun1( short ) ===
# pfun = ctypes.CDLL( './call.so' ).fun1
pfun = lib.fun1
pfun.restype = ctypes.c_double
pfun.argtypes = ( ctypes.c_short, )
x = pfun( 3 )
print 'возвращено значение %f' % x
print '-----------------------------------------------------'

# === const char* fun2( const char* ) ===
#pfun = ctypes.CDLL( './call.so' ).fun2
pfun = lib.fun2
pfun.restype = ctypes.c_char_p
pfun.argtypes = ( ctypes.c_char_p, )
x = pfun( "строка вызова" )
print 'возвращено значение : "%s"' % x
print '-----------------------------------------------------'

# === void* fun3( float, long, char* ) ===
pfun = lib.fun3
pfun.restype = ctypes.c_void_p
pfun.argtypes = ( ctypes.c_float, ctypes.c_long, ctypes.c_char_p )
s = "строка"
x = pfun( 3.1415, -12345, s )
print 'возвращено значение\t= %x' % x
print 'id( строка-параметр )\t= %x' % id( s )
print '-----------------------------------------------------'

Примечание. В примере из листинга 2 используется синтаксис Python 2, так как в Python 3, где строки представляются исключительно в UTF-8, следует использовать типы wchar_t в C коде, и, соответственно, c_wchar_p в Python коде. Декодирование строк для версий 2 и 3 выполняется различными способами. Кроме того, в Python 3 присутствует более строгий контроль соответствия типов параметров, передаваемых функциям.

Проверим работоспособность созданного межязыкового интерфейса:

$ python test.py
-----------------------------------------------------
получен параметр = 3
возвращено значение 3.000000
-----------------------------------------------------
локализация операционной системы: ru_RU.UTF-8
получен параметр [ mbchar* ] : "строка вызова"; длина: в байтах = 25, в символах = 13
возвращено значение : "возвращаемая строка"
-----------------------------------------------------
вызов функции 3-х переменных: "float"=3,141500, "long"=-12345, "char*"="строка"
возвращено значение     = b76b15ac
id( строка-параметр )   = b76b1598
-----------------------------------------------------

В листинге 1 был показан условный пример, демонстрирующий работу с самыми разными вариантами параметров и возвращаемых значений. Теперь мы попробуем решить более реалистичную задачу по считыванию 64-х разрядного счётчика периодов частоты процессора, реализованного, начиная с процессора Pentium-II, и возвращаемого ассемблерной командой RDTSC. Такая функция позволит замерять временные интервалы в ходе выполнения программ с наносекундным разрешением. В листинге 3 приведён пример такой функции на языке С (см. файл rdtsc.c в архиве python_c_interaction.tgz):

Листинг 3. Функция rdtsc()
unsigned long long rdtsc( void ) {          // ctypes.c_ulonglong rdtsc( void )
   unsigned long long int x;
   asm volatile ( "rdtsc" : "=A" (x) );
// вариант для старых версий GCC – просто указываем численный код команды:
// asm volatile ( ".byte 0x0f, 0x31" : "=A" (x) );
   return x;
}

Реализация функции rdtsc() использует такое расширение компилятора GCC как инлайновые ассемблерные вставки. Соберём библиотеку rdtsc.so с помощью следующей команды:

$ gcc -shared -o call.so -fPIC call.c

В листинге 4 представлено тестовое приложение, использующее из кода Python функцию, реализованную на C (см. файл rdtest.py в архиве python_c_interaction.tgz):

Листинг 4. Тестовое приложение для rdtsc()
#!/usr/bin/python
# -*- coding: utf-8 -*-

import ctypes
lib = ctypes.CDLL( './rdtsc.so' )
pfun = lib.rdtsc
pfun.argtypes = () # ctypes.c_short, )
pfun.restype = ctypes.c_ulonglong

for i in 1, 2, 3 :
    print( 'RDTSC = {}'.format( pfun() ) )

Выполнив это приложение, мы получим число периодов частоты процессора с момента последней перезагрузки:

$ python rdtest.py
RDTSC = 81688400845550
RDTSC = 81688400995990
RDTSC = 81688401103470

Этот же код можно исполнить и в Python 3:

$ python3 rdtest.py
RDTSC = 81694859707300
RDTSC = 81694859845670
RDTSC = 81694859918650

Отдельно остановимся на важнейшей особенности этого способа, так как мы не создаём на языке C модуля для последующего экспорта (как это будет делаться во всех других способах), а, напротив, используем искусственный интерфейс к функциям C из языка Python.

Даже из простых представленных примеров видно, что, при использовании ctypes, код для вызова С-функций из Python становится громоздким и малопонятным. Тем не менее, это простейший способ для взаимодействия с C-библиотеками из Python-приложений. Отличительной особенностью ctypes является простота реализации вызовов из уже существующих разделяемых системных библиотек, особенно когда требуется обеспечить вызов одной или нескольких функций из такой библиотеки. Например, вот как могут выглядеть обращения к основным стандартным библиотекам в разных операционных системах. Мы не будем обсуждать особенности правил выполнения вызовов, которые подробно описаны в документации, перечисленной в разделе "Ресурсы":

  • для ОС Windows (расширение .dll будет добавлено автоматически):
    from ctypes import *
    libs = windll.kernel32 // функции системной библиотеки kernel32.dll
                           // используют соглашение о связях stdcall
    print( libs.GetModuleHandleA(None) )
    ...
    libc = cdll.msvcrt     // функции стандартной библиотеки msvcrt.dll
                           // используют соглашение о связях cdecl.dll
    print( libc.time( None ) )
  • для ОС Linux:
    from ctypes import *
    libc = ctypes.CDLL( 'libc.so.6' )
    print( libc.time( None ) )

Здесь же проявляется ещё одна неприятная особенность использования ctypes, то что синтаксис записи связующего кода различается для разных операционных систем (что в общем не свойственно Python), а это сильно затрудняет создание мульти-платформенного кода.


Создание интерфейса модуля

Это второй из названных ранее альтернативных способов использования C-кода из Python. Если в предыдущем случае мы не создавали отдельный модуль (а непосредственно использовали библиотеку), то теперь мы сможем его создать, полностью прописав на языке C создание интерфейса к такому модулю. Наш модуль будет также реализовывать всё ту же функцию rdtsc(), опрашивающую счётчик тактов процессора. Используем тот же файл rdtsc.c с реализацией функции, но дополним его соответствующим файлом с общими определениями - rdtsc.h, который необходим только для организации взаимодействия.

extern unsigned long long rdtsc( void );

Теперь можно вручную (листинг 5) прописать интерфейс для нашего будущего модуля rdtsc в виде файла rdtsc_wrap.c (код можно найти в архиве python_c_interaction.tgz):

Листинг 5. Интерфейс модуля
#include <Python.h>
#include "rdtsc.h"

PyObject* rdtsc_wrap( PyObject* self, PyObject* args ) {
   if( self != NULL ) return NULL;
   return Py_BuildValue( "L", rdtsc() );
}

static PyMethodDef rdtscmethods[] = {      // таблица описания методов модуля
   { "rdtsc", rdtsc_wrap, METH_NOARGS },
   { NULL, NULL }
};

void initrdtsc() {                         // функция инициализации модуля
   Py_InitModule( "rdtsc", rdtscmethods );
}

В коде интерфейса должны присутствовать два обязательных компонента:

  • таблица описания всех методов модуля;
  • функция инициализации модуля (которая практически всегда одинакова, за исключением своего имени);

Правило для Makefile, ответственное за сборку DLL библиотеки, представляющей созданный Python-модуль будет выглядеть так как показано ниже:

        gcc -c -fpic rdtsc_wrap.c rdtsc.c -I/usr/include/python2.7
        ld -shared rdtsc_wrap.o rdtsc.o -lc -o rdtscmodule.so

Теперь можно будет осуществить вызов функции rdtsc() из кода приложения на Python, представленного в листинге 6 (файл ptest.py из архива python_c_interaction.tgz):

Листинг 6. Вызов C-функции c помощью интерфейсного модуля
#!/usr/bin/python -O
# -*- coding: utf-8 -*-
from rdtsc import rdtsc
from calibr import calibr

counter = []
for i in range( 5 ):
    counter.append( rdtsc() )
print "счётчик процессорных циклов:\n", counter

arg = [ 0, 10, 100, 1000, 10000, 100000 ]
msg = "калибровка последовательных вызовов:"
for i in arg:
    s = " %s(%i)" % ( str( calibr( i ) ), i )
    msg = msg + s
print msg

Попутно в модуле calibr была реализована функция calibr(), нужная при измерения очень малых интервалов. Пример этой функции показан в листинге 7 и файле calibr.py в архиве python_c_interaction.tgz.

Листинг 7. Функция калибровки
#!/usr/bin/python
# -*- coding: utf-8 -*-
from rdtsc import rdtsc

def calibr( args = 10 ):
    sum = 0L
    if int( args ) <= 0: n = 10
    else: n = int( args )
    m = n
    while n > 0 :
        cf = -( rdtsc() - rdtsc() )
        sum = sum + cf
        n = n - 1
    return sum / m
...
if __name__ == "__main__":
    arg = [ 2, 5, 10, 20, 50, 100 ]
    msg = "калибровка последовательных вызовов:"
    for i in arg:
        s = " %s(%i)" % ( str( calibr( i ) ), i )
        msg = msg + s
    print msg

Эта функция определяет разность значений, полученных из двух последовательных вызовов rdtsc(), непосредственно следующих друг за другом, и показывает временные затраты (в тактах частоты процессора) на выполнение одиночного вызова самой функции rdtsc().

Чтобы исключить дисперсию из-за загруженности процессора в многозадачной системе это значение усредняется по большому числу повторных измерений (при усреднении по 10 и более замеров результат можно считать стабильным).

Теперь можно проверить сценарий из листинга 6:

$ python -O ptest.py
счётчик процессорных циклов:
[32794250939400L, 32794250943940L, 32794250945180L, 32794250946280L, 32794250947320L]
калибровка последовательных вызовов: 520(0) 478(10) 445(100) 441(1000) ... 422(100000)

Для сравнения воспользуемся аналогичной тестовой программой, написанной на языке C (файл ctest.c из архива python_c_interaction.tgz):

Листинг 8. Эталонная программа с аналогичной функциональностью, написанная на C
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include "rdtsc.h"

#define NUMB 10
static unsigned calibr( int rep ) {
   uint32_t n, m, sum = 0;
   n = m = ( rep <= 0 ? NUMB : rep );
   while( n-- ) {
      uint64_t cf, cs;
      cf = rdtsc();
      cs = rdtsc();
      sum += (uint32_t)( cs - cf );
   }
   return sum / m;
}

int main( int argc, char **argv, char **envp ) {
   printf( "число процессорных тактов = %llu\n", rdtsc() );
   printf( "калибровка последовательных вызовов:" );
   printf( " %lu(0)", calibr( 0 ) );
   int n;
   for( n = 10; n <= 100000; n*=10 )
      printf( " %lu(%d)", calibr( n ), n );
   printf( "\n");
   exit( EXIT_SUCCESS );
};

Проверим работу этой реализации (тесты выполнялись последовательно друг за другом, поэтому значения счётчика процессорных тактов имеют сходный порядок):

$ ./ctest
число процессорных тактов = 32794140948560
калибровка последовательных вызовов: 193(0) 191(10) 191(100) 191(1000) ... 142(100000)

Имея два различных варианта реализации (Python и C), мы можем сравнить время выполнения функции rdtsc() в Python – порядка 440 тактов с реализацией на C – около 190 тактов. Разница всего в 2.3 раза — это очень неплохой результат для интерпретирующего языка.


Заключение

В этой статье были описаны 2 возможных альтернативных способа использования C-кода из программы Python.

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

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


Загрузка

ОписаниеИмяРазмер
интеграция Python и С/С++python_с_interaction.tgz26KB

Ресурсы

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


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

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


  • Bluemix

    Узнайте больше информации о платформе IBM Bluemix, создавайте приложения, используя готовые решения!

  • developerWorks Premium

    Эксклюзивные инструменты для построения вашего приложения. Узнать больше.

  • Библиотека документов

    Более трех тысяч статей, обзоров, руководств и других полезных материалов.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Open source
ArticleID=957907
ArticleTitle=Тонкости использования языка Python: Часть 6. Способы интеграции Python и С/С++ приложений
publish-date=12182013