17
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.
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.
To understand the calling functions, you can refer to the DynamsoftBarcodeReader.h
file.
DBR_CreateInstance = dbr.DBR_CreateInstance
DBR_CreateInstance.restype = c_void_p
instance = dbr.DBR_CreateInstance()
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
.
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')))
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)))]
DBR_FreeTextResults = dbr.DBR_FreeTextResults
DBR_FreeTextResults.argtypes = [POINTER(POINTER(TextResultArray))]
if bool(pResults):
DBR_FreeTextResults(byref(pResults))
DBR_DestroyInstance = dbr.DBR_DestroyInstance
DBR_DestroyInstance.argtypes = [c_void_p]
DBR_DestroyInstance(instance)
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.
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.
17