Python Ctypes for Loading and Calling Shared Libraries
Tags: python, ctypes, cmake
Published: 2021-07-19 02:53:28

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](https://docs.python.org/3/library/ctypes.html) 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](https://www.dynamsoft.com/barcode-reader/downloads/), 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:

```python
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:

```bash
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

```python
DBR_CreateInstance = dbr.DBR_CreateInstance
DBR_CreateInstance.restype = c_void_p
instance = dbr.DBR_CreateInstance()
```

#### Step 2: Set the License Key

```python
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](https://www.dynamsoft.com/customer/license/trialLicense?product=dbr) 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 

```python
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

```python
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：

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

```python
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

```python
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
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`:

```c
#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:

```c
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`:

```c
#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:

```python
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: 

```python
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:

```python
# 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](https://github.com/yushulx/python-ctypes-barcode-shared-library)