Fedyashov's Blog

Just another WordPress.com site

Python ctypes as a handy tool for C/C++ developers

I have recently learned how useful Python ctypes library might be when it comes to cross-platform testing of dynamic libraries. ctypes handles library loading and allows easy function invocation in case if library exposes C-like interfaces. In addition, library provides a way of using non-trivial data types as function call arguments (such as struct, union, C-style arrays and their combinations).

Solution is cross-platform, it significantly simplifies development and testing of new functions addition to existing dynamic libraries at early stages, so that there are no other clients for those functions ready yet. Also, it can be used as a good sanity measure for automated build regression checks.

As an example, here is a small use case: suppose I have to check the following function from a dynamic library (.dll for Windows, .so on Linux) written in C/C++:

int get_ips(const unsigned short af, struct ipaddress_info ** ppaddrinfo);

Where ipaddress_info is defined like this:

typedef struct ipaddress_info {
    unsigned short family;
    unsigned char addr[16];
    unsigned int scope_id;
} IPADDRESS_INFO;

So given the definition above – you’d have a DLL on Windows (or a shared library on Linux) that exports get_ips function. And here is the sample Python script that is capable to load that library, perform a call to get_ips and display the contents of a returned array of structures:

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 invokation 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__":
    # you'd probably have to use LD_LIBRARY_PATH to make this work on Linux
    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))

The first argument is a library file name; second – is the location from which library load should be performed (it is sufficient on Windows, on Linux you might need to deal with LD_LIBRARY_PATH if your library has its own dependencies).

For simplicity, I removed most of error handling code and some not very interesting things like calling another library function to perform memory cleanup. Also, code assumes cdecl calling convention (which actually was my case for both tested platforms)

If you’re interested in Russian version of this entry – take a look here on my impulse9 blog

About these ads

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: