Python Ctypes for Loading and Calling Shared Libraries

Xiao Ling - Jul 19 '21 - - Dev Community

As the original writer of Dynamsoft Barcode Reader for Python (pip install dbr), I prefer using CPython and Dynamsoft C/C++ Barcode SDK to bring barcode detection APIs to Python. However, Ctypes is also worth to be explored. It allows developers to call C functions of shared libraries in pure Python code. The article goes through the steps of how to invoke Dynamsoft C/C++ Barcode APIs by Python Ctypes.

Attempting to Call C Functions of Dynamsoft Barcode SDK in Pure Python

Assume you have downloaded the C/C++ packages, which contains *.dll and *.so files for Windows and Linux, from Dynamsoft website. We copy the *.dll and *.so files to the same folder of our Python project.

In the Python file, we import the Ctypes library and then load the Dynamsoft shared library:

import os
import platform
from ctypes import *

dbr = None
if 'Windows' in system:         
    os.environ['path'] += ';' + os.path.join(os.path.abspath('.'), r'<subfolder path>')
    dbr = windll.LoadLibrary('DynamsoftBarcodeReaderx64.dll')
else:
    dbr = CDLL(os.path.join(os.path.abspath('.'), '<libDynamsoftBarcodeReader.so path>'))
Enter fullscreen mode Exit fullscreen mode

The DynamsoftBarcodeReaderx64.dll depends on vcom110.dll. If vcom110.dll is missed, you will get the following error:

Traceback (most recent call last):
  File ".\failure.py", line 80, in <module>
    dbr = windll.LoadLibrary('DynamsoftBarcodeReaderx64.dll')
  File "C:\Python37\lib\ctypes\__init__.py", line 442, in LoadLibrary
    return self._dlltype(name)
  File "C:\Python37\lib\ctypes\__init__.py", line 364, in __init__
    self._handle = _dlopen(self._name, mode)
OSError: [WinError 126] The specified module could not be found
Enter fullscreen mode Exit fullscreen mode

For Linux, the path of libDynamsoftBarcodeReader.so is enough.

Steps to Decode a Barcode Image with Ctypes

To understand the calling functions, you can refer to the DynamsoftBarcodeReader.h file.

Step 1: Initialize the Dynamsoft Barcode Reader

DBR_CreateInstance = dbr.DBR_CreateInstance
DBR_CreateInstance.restype = c_void_p
instance = dbr.DBR_CreateInstance()
Enter fullscreen mode Exit fullscreen mode

Step 2: Set the License Key

DBR_InitLicense = dbr.DBR_InitLicense
DBR_InitLicense.argtypes = [c_void_p, c_char_p] 
DBR_InitLicense.restype = c_int
ret = DBR_InitLicense(instance, c_char_p('LICENSE-KEY'.encode('utf-8')))
Enter fullscreen mode Exit fullscreen mode

Get a valid license key from Dynamsoft website and then update c_char_p('LICENSE-KEY'.encode('utf-8')).

When converting the Python string to char *, you need to encode it to utf-8 first. Otherwise, you will get TypeError: bytes or integer address expected instead of str instance.

Step 3: Call the Decoding API

DBR_DecodeFile = dbr.DBR_DecodeFile
DBR_DecodeFile.argtypes = [c_void_p, c_char_p, c_char_p]
DBR_DecodeFile.restype = c_int
ret = DBR_DecodeFile(instance, c_char_p('test.png'.encode('utf-8')), c_char_p(''.encode('utf-8')))
Enter fullscreen mode Exit fullscreen mode

Step 4: Get the Barcode Result

pResults = POINTER(TextResultArray)()
DBR_GetAllTextResults = dbr.DBR_GetAllTextResults
DBR_GetAllTextResults.argtypes = [c_void_p, POINTER(POINTER(TextResultArray))]
DBR_GetAllTextResults.restype = c_int
ret = DBR_GetAllTextResults(instance, byref(pResults))
print(ret)

resultsCount = pResults.contents.resultsCount
print(resultsCount)
results = pResults.contents.results
print(results)

if bool(results):
    for i in range(resultsCount):
        result = results[i] 
        if bool(result):
            print(result)
            print('Format: %s' % result.contents.barcodeFormatString.decode('utf-8'))                   
            print('Text: %s' % result.contents.barcodeText.decode('utf-8'))
Enter fullscreen mode Exit fullscreen mode

This is the toughest part of the whole process. The result is a TextResultArray structure, which contains a list of TextResult structures. Each TextResult structure contains the barcode format, the barcode text, and other information. To access the barcode decoding results, we need to define all relevant C structures in Python:

class SamplingImageData(Structure):
    _fields_ = [("bytes", POINTER(c_byte)), ("width", c_int), ("height", c_int)]

class ExtendedResult(Structure):
    _fields_ = [("resultType", c_uint),
                ("barcodeFormat", c_uint),
                ("barcodeFormatString", c_char_p),
                ("barcodeFormat_2", c_uint),
                ("barcodeFormatString_2", c_char_p),
                ("confidence", c_int),
                ("bytes", POINTER(c_byte)),
                ("bytesLength", c_int),
                ("accompanyingTextBytes", POINTER(c_byte)),
                ("accompanyingTextBytesLength", c_int),
                ("deformation", c_int),
                ("detailedResult", c_void_p),
                ("samplingImage", SamplingImageData),
                ("clarity", c_int),
                ("reserved", c_char * 40),
                ]

class LocalizationResult(Structure):
    _fields_ = [("terminatePhase", c_uint), 
    ("barcodeFormat", c_uint),
    ("barcodeFormatString", c_char_p),
    ("barcodeFormat_2", c_uint),
    ("barcodeFormatString_2", c_char_p),
    ("x1", c_int),
    ("y1", c_int),
    ("x2", c_int),
    ("y2", c_int),
    ("x3", c_int),
    ("y3", c_int),
    ("x4", c_int),
    ("y4", c_int),
    ("angle", c_int),
    ("moduleSize", c_int),
    ("pageNumber", c_int),
    ("regionName", c_char_p),
    ("documentName", c_char_p),
    ("resultCoordinateType", c_uint),
    ("accompanyingTextBytes", POINTER(c_byte)),
    ("accompanyingTextBytesLength", c_int),
    ("confidence", c_int),
    ("reserved", c_char * 52),
    ]

class TextResult(Structure):
    _fields_ = [("barcodeFormat", c_uint), 
    ("barcodeFormatString", c_char_p), 
    ("barcodeFormat_2", c_uint), 
    ("barcodeFormatString_2", c_char_p), 
    ("barcodeText", c_char_p), 
    ("barcodeBytes", POINTER(c_byte)),
    ("barcodeBytesLength", c_int),
    ("localizationResult", POINTER(LocalizationResult)),
    ("detailedResult", c_void_p),
    ("resultsCount", c_int),
    ("results", POINTER(POINTER(ExtendedResult))),
    ("exception", c_char_p),
    ("isDPM", c_int),
    ("isMirrored", c_int),
    ("reserved", c_char * 44),
    ]


class TextResultArray(Structure):
    _fields_= [("resultsCount",c_int),
              ("results", POINTER(POINTER(TextResult)))]
Enter fullscreen mode Exit fullscreen mode

Step 5: Free the memory of Barcode Result

DBR_FreeTextResults = dbr.DBR_FreeTextResults
DBR_FreeTextResults.argtypes = [POINTER(POINTER(TextResultArray))]
if bool(pResults):
    DBR_FreeTextResults(byref(pResults)) 
Enter fullscreen mode Exit fullscreen mode

Step 6: Destroy the Dynamsoft Barcode Reader

DBR_DestroyInstance = dbr.DBR_DestroyInstance
DBR_DestroyInstance.argtypes = [c_void_p]
DBR_DestroyInstance(instance)
Enter fullscreen mode Exit fullscreen mode

Testing the program

So far, we have successfully implemented a Python barcode reader with Ctypes. Nevertheless, running the script outputs nothing. The result count is great than zero, but the bool(results) returns false, which means the barcode decoding results are empty. It is impossible apparently. But unfortunately, I have not yet figured out where the bug is. Probably it is caused by the memory copy of the nested C structures. To avoid segmentation violation, it is better to write a bit of C code to simplify C structures defined in Python.

Writing a Bit of C Code to Simplify Ctypes Calls

We create a CMake library project named bridge to implement two methods dbr_get_results and dbr_free_results instead of DBR_GetAllTextResults and DBR_FreeTextResults. The C code is written in bridge.c.

The CMakeLists.txt file is as follows:

cmake_minimum_required(VERSION 3.0.0)

project(bridge VERSION 0.1.0)

INCLUDE_DIRECTORIES("${CMAKE_CURRENT_SOURCE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/include")
if (CMAKE_HOST_WIN32)
    LINK_DIRECTORIES("${CMAKE_CURRENT_SOURCE_DIR}/lib/Windows")
else()
    LINK_DIRECTORIES("${CMAKE_CURRENT_SOURCE_DIR}/lib/Linux")
endif()
add_library(${PROJECT_NAME} SHARED bridge.c)

if(CMAKE_HOST_WIN32)
    target_link_libraries (${PROJECT_NAME} "DBRx64")
else()
    target_link_libraries (${PROJECT_NAME} "DynamsoftBarcodeReader")
endif()
Enter fullscreen mode Exit fullscreen mode

To export functions for Windows and Linux, we define a macro in bridge.h:

#if !defined(_WIN32) && !defined(_WIN64)
#define EXPORT_API
#else
#define EXPORT_API __declspec(dllexport)
#endif
Enter fullscreen mode Exit fullscreen mode

In addition, we define the C structures and functions, which will be used by Python Ctypes:

typedef struct {
    char* format;
    char* text;
} ResultInfo;

typedef struct {
    int size;
    ResultInfo** pResultInfo;
} ResultList;

EXPORT_API ResultList* dbr_get_results(void* barcodeReader);
EXPORT_API void dbr_free_results(ResultList* resultList);
Enter fullscreen mode Exit fullscreen mode

In bridge.c, we add the implementation of dbr_get_results and dbr_free_results:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "bridge.h"

ResultList* dbr_get_results(void* barcodeReader)
{
    TextResultArray *pResults;
    int ret = DBR_GetAllTextResults(barcodeReader, &pResults);
    int count = pResults->resultsCount;
    TextResult **results = pResults->results;

    ResultInfo** pResultInfo = (ResultInfo**)malloc(sizeof(ResultInfo*) * count);
    ResultList* resultList = (ResultList*)malloc(sizeof(ResultList));
    resultList->size = count;
    resultList->pResultInfo = pResultInfo;

    for (int i = 0; i < count; i++)
    {
        TextResult* pResult = results[i];
        ResultInfo* pInfo = (ResultInfo*)malloc(sizeof(ResultInfo));
        pInfo->format = NULL;
        pInfo->text = NULL;
        pResultInfo[i] = pInfo;
        pInfo->format = (char *)calloc(strlen(pResult->barcodeFormatString) + 1, sizeof(char));
        strncpy(pInfo->format, pResult->barcodeFormatString, strlen(pResult->barcodeFormatString));
        pInfo->text = (char *)calloc(strlen(pResult->barcodeText) + 1, sizeof(char));
        strncpy(pInfo->text, pResult->barcodeText, strlen(pResult->barcodeText));
    }

    DBR_FreeTextResults(&pResults);

    return resultList;
}

void dbr_free_results(ResultList* resultList)
{
    int count = resultList->size;
    ResultInfo** pResultInfo = resultList->pResultInfo;

    for (int i = 0; i < count; i++) 
    {   
        ResultInfo* resultList = pResultInfo[i];
        if (resultList) 
        {
            if (resultList->format)
                free(resultList->format);
            if (resultList->text)
                free(resultList->text);

            free(resultList);
        }
    }

    if (pResultInfo)
        free(pResultInfo);
}
Enter fullscreen mode Exit fullscreen mode

Once the bridge library build is done, we go back to the Python script. A new library file bridge.dll/bridge.so is ready for load:

dbr = None
bridge = None
if 'Windows' in system:                   
    os.environ['path'] += ';' + os.path.join(os.path.abspath('.'), r'bridge\lib\Windows')
    dbr = windll.LoadLibrary('DynamsoftBarcodeReaderx64.dll')
    bridge = windll.LoadLibrary(os.path.join(os.path.abspath('.'), r'bridge\build\Debug\bridge.dll'))
else:
    dbr = CDLL(os.path.join(os.path.abspath('.'), 'bridge/lib/Linux/libDynamsoftBarcodeReader.so'))
    bridge = CDLL(os.path.join(os.path.abspath('.'), 'bridge/build/libbridge.so'))
Enter fullscreen mode Exit fullscreen mode

The library loading sequence is vital for Linux: libDynamsoftBarcodeReader.so first, then libbridge.so. If the library is loaded in the wrong order, the Python code will fail to work on Linux.

The C structures are now much simpler and cleaner than the structures defined in the previous step:

class ResultInfo(Structure):
    _fields_ = [("format", c_char_p), ("text", c_char_p)]

class ResultList(Structure):
    _fields_ = [("size", c_int), ("pResultInfo", POINTER(POINTER(ResultInfo)))]
Enter fullscreen mode Exit fullscreen mode

The Python code for getting and destroying barcode decoding results is therefore changed to:

# dbr_get_results  
dbr_get_results = bridge.dbr_get_results
dbr_get_results.argtypes = [c_void_p]
dbr_get_results.restype = c_void_p
address = dbr_get_results(instance)
data = cast(address, POINTER(ResultList))
size = data.contents.size
results = data.contents.pResultInfo

for i in range(size):   
    result = results[i]                
    print('Format: %s' % result.contents.format.decode('utf-8'))                   
    print('Text: %s' % result.contents.text.decode('utf-8'))

# dbr_free_results
dbr_free_results = bridge.dbr_free_results
dbr_free_results.argtypes = [c_void_p]
if bool(address):
    dbr_free_results(address)  
Enter fullscreen mode Exit fullscreen mode

Finally, we can successfully run the Python barcode decoding application.

Source Code

https://github.com/yushulx/python-barcode-qrcode-sdk/tree/main/examples/ctypes

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player