суббота, 15 октября 2011 г.

Python ctypes в помощь С/С++ разработчику


Недавно открыл для себя Python ctypes как весьма удобный способ быстрого тестирования функций из динамических библиотек. ctypes позволяет произвести загрузку динамической библиотеки и реализовать вызов экспортируемых функций, при условии использования C-интерфейсов; при этом, библиотека предоставляет достаточно средств для передачи нетривиальных типов данных, таких как структуры, объединения, массивы и их комбинации.

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

В качестве примера привожу скрипт, которым я тестировал функцию с таким прототипом:
int get_ips(const unsigned short af, struct ipaddress_info ** ppaddrinfo);
Структура ipaddress_info выглядит так:
typedef struct ipaddress_info {
    unsigned short family;
    unsigned char addr[16];
    unsigned int scope_id;
} IPADDRESS_INFO;
Следующий скрипт (я использовал Python 3.2 в Windows 7 и Ubuntu 11.10) вызывает функцию get_ips из динамической библиотеки и отображает результат, включая содержимое массива адресов, построенного тестируемой функцией:

import os
import sys
from ctypes import *
from socket import AF_INET, AF_INET6, AF_UNSPEC

# Python representation of a C struct defined
# in a shared library being tested
class ipaddress_info(Structure):
    _fields_ = [
        ("family",      c_ushort),
        ("addr",        c_ubyte * 16),
        ("scope_id",    c_uint),
    ]

def bytes2str(v):
    s = ""
    for x in v:
        s += "%.02X " % (x)
    return s

class dynlib_bridge:
    def __init__(self, dll_path, working_dir="."):
        tmp = os.getcwd()
        os.chdir(working_dir)
        self.invoke = cdll.LoadLibrary(dll_path)
        os.chdir(tmp)
class test_case_base:
    def __init__(self, real_values):
        self.__inout__ = dict()
        self.__result__ = None
        for arg_name, arg_value in real_values.items():
            self.__inout__[arg_name] = arg_value
    def actual(self, name):
        return self.__inout__[name]
    def result(self):
        return self.__result__
    def setResult(self, v):
        self.__result__ = v
        
# performs a positive test case
def test_pos(testobj):
    result = testobj.run()
    if result:
        print ("[tc+] OK  :", testobj.describe())
    else:
        print ("[tc+] FAIL:", testobj.describe())    

# performs a negative test case
def test_neg(testobj):
    result = testobj.run()
    if not result:
        print ("[tc-] OK  :", testobj.describe())
    else:
        print ("[tc-] FAIL:", testobj.describe())    

# wraps invocation of a shared library function
class get_ips(test_case_base):
    def __init__(self, dll, af):
        super(get_ips, self).__init__({
            'af': c_ushort(af),
            'ppaddrinfo': POINTER(ipaddress_info)(),
            })
        self.dll = dll
    def run(self):
        invoke_result = self.dll.invoke.get_ips(
            self.actual('af'), 
            byref(self.actual('ppaddrinfo')))
        self.setResult(invoke_result)
        return (invoke_result > 0)
    def describe(self):
        p = self.actual('ppaddrinfo')
        ips = []
        if self.result() > 0:
            for i in range(self.result()):
                ips.append("#%d: family = %d, scope = %d, %s" % (
                    i, 
                    p[i].family, 
                    p[i].scope_id, 
                    bytes2str(p[i].addr)))
        s = "result: %.02d\n%s" % (self.result(), "\n".join(ips))
        return s     
if __name__ == "__main__":
    path_to_lib = sys.argv[1]
    path_to_env = sys.argv[2]
    print ("lib path :", path_to_lib)
    print ("env path :", path_to_env)

    dll = dynlib_bridge(path_to_lib, working_dir=path_to_env)
    
    test_pos(get_ips(dll, AF_UNSPEC))
    test_pos(get_ips(dll, AF_INET))
    test_pos(get_ips(dll, AF_INET6))
    test_neg(get_ips(dll, 99))

Первый параметр - полный путь к тестируемой библиотеке. Второй параметр - путь из которого будет происходить загрузка (для Linux этого может оказаться недостаточно - нужно также настроить LD_LIBRARY_PATH, но это уже совсем другая тема).

Для упрощения примера из него удалена обработка ошибок и вызовы других функций тестируемой библиотеки (в том числе функции освобождения памяти, выделенной в get_ips). Также следует обратить внимание, что тестируемая библиотека для всех платформ была 32-битной и интересующие функции использовали cdecl соглашение о вызове.

Комментариев нет:

Отправить комментарий