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

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:

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

Steps to Decode a Barcode Image with Ctypes

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('LICENSE-KEY'.encode('utf-8')))

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

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

Writing a Bit of C Code to Simplify Ctypes Calls

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

Originally published at https://www.dynamsoft.com on July 12, 2021.

Manager of Dynamsoft Open Source Projects | Tech Lover