Python Ctypes for Loading and Calling Shared Libraries

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>'))

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

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()

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('DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ=='.encode('utf-8')))

Get a valid license key from Dynamsoft website and then update c_char_p('DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ=='.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')))

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'))

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)))]

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)) 

Step 6: Destroy the Dynamsoft Barcode Reader

DBR_DestroyInstance = dbr.DBR_DestroyInstance
DBR_DestroyInstance.argtypes = [c_void_p]
DBR_DestroyInstance(instance)

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()

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

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);

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);
}

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'))

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)))]

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)  

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

Source Code

https://github.com/yushulx/python-ctypes-barcode-shared-library