Integrating Third-Party iOS Frameworks into a Qt6 Barcode and QR Code Scanner
Tags: qt, ios, qrcode, cpp
Published: 2024-04-09 06:21:29

In a previous article, we developed a [barcode and QR code scanner](https://www.dynamsoft.com/codepool/qt-qml-qr-code-scanner.html) for **Windows** and **Android** using Qt6. In this follow-up, we'll extend the project to include support for **iOS** devices. To achieve this, we'll integrate the Dynamsoft iOS Barcode Reader SDK. To facilitate seamless communication between the SDK and Qt6, we will create a C wrapper. This wrapper will act as a bridge, providing all the necessary functions to ensure compatibility with our existing Qt6 project without requiring any code changes.


## Prerequisites
- [Qt Online Installer](https://www.qt.io/download-qt-installer-oss)
- [Dynamsoft Barcode Reader Trial License](https://www.dynamsoft.com/customer/license/trialLicense/?product=dbr)


## Exported Symbols from the Dynamsoft iOS Barcode Reader SDK
The Dynamsoft Barcode Reader SDK for iOS exclusively offers Objective-C APIs, which cannot be directly integrated into a Qt C++ project. To confirm this, we can employ the `nm` command to inspect the exported symbols within the `DynamsoftBarcodeReader.framework`:

```bash
nm -gU /path/to/DynamsoftBarcodeReader.framework/DynamsoftBarcodeReader
```

![exported symbols of Dynamsoft iOS Barcode SDK](https://www.dynamsoft.com/codepool/img/2024/04/ios-framework-exported-symbol.jpg)


In the list of exported symbols of a binary, the `_OBJC_` prefix denotes symbols associated with the Objective-C runtime, indicating their reliance on Objective-C specific structures and conventions.

## Creating a C Wrapper for Bridging Objective-C with Qt C++
To resolve the issue of API incompatibility, we initiate a `Framework` project in Xcode.

![Create a new iOS Framework project in Xcode](https://www.dynamsoft.com/codepool/img/2024/04/xcode-framework-project.jpg)

The project structure is as follows:

![iOS Framework project structure](https://www.dynamsoft.com/codepool/img/2024/04/xcode-framework-structure.jpg)

This structure includes a `bridge.h` file for defining the C functions and a `bridge.m` file for implementing these functions. Additionally, the `DynamsoftBarcodeReader.framework` is integrated into the project. To ensure the `DynamsoftBarcodeReader.framework`'s header files are accessible, we must configure the search path in the project settings.

```objectivec
#import <DynamsoftBarcodeReader/DynamsoftBarcodeReader.h>
```

![Add search path for header files](https://www.dynamsoft.com/codepool/img/2024/04/xcode-framework-search-path.jpg)

Leveraging the `DynamsoftBarcodeReader.h` header file—similarly used in Windows and Android projects—we proceed to declare C functions in `bridge.h`:

```c
#ifndef bridge_h
#define bridge_h

#include <stdio.h>
#include <string.h>

#ifdef __cplusplus
extern "C"
{
#endif
    typedef struct
    {
        void *instance;
        void *result;
    } BarcodeReader;

    const char *DBR_GetVersion(void);
    int DBR_InitLicense(const char *pLicense, char errorMsgBuffer[], const int errorMsgBufferLen);
    void *DBR_CreateInstance(void);
    void DBR_DestroyInstance(void *barcodeReader);
    int DBR_InitRuntimeSettingsWithString(void *barcodeReader, const char *content, const ConflictMode conflictMode, char errorMsgBuffer[], const int errorMsgBufferLen);
    int DBR_GetAllTextResults(void *barcodeReader, TextResultArray **pResults);
    void DBR_FreeTextResults(TextResultArray **pResults);
    int DBR_DecodeBuffer(void *barcodeReader, const unsigned char *pBufferBytes, const int width, const int height, const int stride, const ImagePixelFormat format, const char *pTemplateName);

#ifdef __cplusplus
}
#endif

#endif 
```

The primary distinction here is the introduction of the `BarcodeReader` struct, designated to hold pointers to Objective-C objects.

Next, we will implement the functions within the `bridge.m` file, employing Objective-C APIs.

### Implementing C Functions Using Objective-C APIs

- `const char *DBR_GetVersion(void)`

    This method returns the version of the Dynamsoft Barcode Reader SDK.
    
    
    ```objectivec
    const char *DBR_GetVersion(void)
    {
        NSString *version = [DynamsoftBarcodeReader getVersion];
        return [version UTF8String];
    }
    ```

- `int DBR_InitLicense(const char* pLicense, char errorMsgBuffer[], const int errorMsgBufferLen)`

    This method initializes the license of the Dynamsoft Barcode Reader SDK.
    
    ```objectivec
    @interface LicenseVerifier : NSObject <DBRLicenseVerificationListener>
    @end
    
    @implementation  LicenseVerifier
    
    - (void)DBRLicenseVerificationCallback:(_Bool)isSuccess error:(NSError *)error {
        NSLog(@"License Verification Success: %d, Error: %@", isSuccess, error.localizedDescription);
    }
    @end
    
    int DBR_InitLicense(const char* pLicense, char errorMsgBuffer[], const int errorMsgBufferLen)
    {
        @autoreleasepool {
            NSString *licenseKey = [NSString stringWithUTF8String:pLicense];
            LicenseVerifier *verifier = [[LicenseVerifier alloc] init];
            
            [DynamsoftBarcodeReader initLicense:licenseKey verificationDelegate: verifier];
            
            return 0;
        }
        
    }
    ```

    We have modified the license verification mechanism: the C function does not await the asynchronous callback. Regardless of whether the license verification is successful, it always returns 0. The license verification result is printed in the `LicenseVerifier` class, which adheres to the `DBRLicenseVerificationListener` protocol. The `DBRLicenseVerificationCallback` method is implemented to process the license verification result. Utilizing `@autoreleasepool` ensures that any Objective-C objects created within are properly released, thereby preventing memory leaks.

- `void* DBR_CreateInstance(void)`

    This method creates an instance of the Dynamsoft Barcode Reader SDK.
    
    ```objectivec
    void* DBR_CreateInstance(void)
    {
        @autoreleasepool {
            DynamsoftBarcodeReader *barcodeReader = [[DynamsoftBarcodeReader alloc] init];
            
            BarcodeReader *brInstance = malloc(sizeof(BarcodeReader));
            brInstance->instance = (__bridge_retained void*)barcodeReader;
            brInstance->result = NULL; // Initially, there are no results
            
            return brInstance;
        }
    
    }
    ```
    
    The `__bridge_retained` keyword casts an Objective-C pointer to a C pointer, transferring ownership to the caller.

- `void DBR_DestroyInstance(void* barcodeReader)` 

    This method destroys an instance of the Dynamsoft Barcode Reader SDK. 

    ```objectivec
    void DBR_DestroyInstance(void* barcodeReader)
    {
        @autoreleasepool {
            if (barcodeReader != NULL) {
                BarcodeReader *brInstance = (BarcodeReader *)barcodeReader;
                DynamsoftBarcodeReader *barcodeReader = (__bridge_transfer DynamsoftBarcodeReader *)brInstance->instance;
                barcodeReader = nil;
            
                if (brInstance->result != NULL) {
                    brInstance->result = nil;
                }
                
                free(brInstance); 
            }
        }
    }
    ```

    The `__bridge_transfer` keyword casts a C pointer back to an Objective-C pointer, transferring ownership back to ARC (Automatic Reference Counting), thus allowing for proper memory management.

- `int DBR_InitRuntimeSettingsWithString(void* barcodeReader, const char* content, const ConflictMode conflictMode, char errorMsgBuffer[], const int errorMsgBufferLen)`

    This method initializes the runtime settings of the Dynamsoft Barcode Reader SDK.
    
    ```objectivec
    int DBR_InitRuntimeSettingsWithString(void* barcodeReader, const char* content, const ConflictMode conflictMode, char errorMsgBuffer[], const int errorMsgBufferLen)
    {
        @autoreleasepool {
            if (barcodeReader == NULL) return -1;
            
            BarcodeReader *brInstance = (BarcodeReader *)barcodeReader;
            DynamsoftBarcodeReader *reader = (__bridge DynamsoftBarcodeReader*)brInstance->instance;
            NSString *settings = [NSString stringWithUTF8String:content];
            
            NSError *error = nil;
            
            [reader initRuntimeSettingsWithString:settings conflictMode:(EnumConflictMode)conflictMode error:&error];
            
            if (error) {
                return -1;
            }
            
            return 0;
        }
    }
    ```

    The `__bridge` keyword indicates to the compiler that the pointer is being cast without changing the ownership of the object.

- `int DBR_DecodeBuffer(void* barcodeReader, const unsigned char* pBufferBytes, int width, int height, int stride, ImagePixelFormat format, const char* pTemplateName)`

    This method decodes buffer data for barcode recognition. 
    
    ```objectivec
    int DBR_DecodeBuffer(void* barcodeReader, const unsigned char* pBufferBytes, int width, int height, int stride, ImagePixelFormat format, const char* pTemplateName) {
        @autoreleasepool {
            if (barcodeReader == NULL) return -1;
    
            BarcodeReader *brInstance = (BarcodeReader *)barcodeReader;
            DynamsoftBarcodeReader *reader = (__bridge DynamsoftBarcodeReader*)brInstance->instance;
            
            NSData *bufferBytes = [NSData dataWithBytes:pBufferBytes length:stride * height]; 
            
            NSError *error = nil;
    
            EnumImagePixelFormat objcFormat = (EnumImagePixelFormat)format;
            
            NSArray<iTextResult*>* results = [reader decodeBuffer:bufferBytes withWidth:width height:height stride:stride format:objcFormat error:&error];
            
            brInstance->result = (__bridge_retained void*)results;
    
            if (error) {
                return -1;
            }
    
            return 0;
        }
    }
    ```

    The results are stored in the `BarcodeReader` struct. The `__bridge_retained` keyword transfers the ownership of the `NSArray<iTextResult*>*` to the C pointer.

- `DBR_GetAllTextResults(void *barcodeReader, TextResultArray **pResults)`

    This method converts Objective-C `NSArray<iTextResult*>*` to C `TextResultArray **`.
    
    ```objectivec
    int DBR_GetAllTextResults(void *barcodeReader, TextResultArray **pResults) {
        @autoreleasepool {
            if (barcodeReader == NULL || pResults == NULL) return -1;
            
            BarcodeReader *brInstance = (BarcodeReader *)barcodeReader;
            NSArray<iTextResult*> *results = (__bridge NSArray<iTextResult*>*)brInstance->result;
            
            TextResultArray *resultArray = (TextResultArray *)malloc(sizeof(TextResultArray));
            resultArray->resultsCount = (int)[results count];
            resultArray->results = (TextResult **)malloc(sizeof(TextResult *) * resultArray->resultsCount);
            
            for (NSInteger i = 0; i < [results count]; i++) {
                iTextResult *iResult = [results objectAtIndex:i];
                
                TextResult *textResult = (TextResult *)malloc(sizeof(TextResult));
                textResult->barcodeFormatString = strdup([iResult.barcodeFormatString UTF8String]);
                textResult->barcodeText = strdup([iResult.barcodeText UTF8String]);
                
                LocalizationResult *locResult = (LocalizationResult *)malloc(sizeof(LocalizationResult));
                NSArray *points = iResult.localizationResult.resultPoints;
                if (points) {
                    CGPoint point0 = [points[0] CGPointValue];
                    locResult->x1 = (int)point0.x;
                    locResult->y1 = (int)point0.y;
                    
                    CGPoint point1 = [points[1] CGPointValue];
                    locResult->x2 = (int)point1.x;
                    locResult->y2 = (int)point1.y;
                    
                    CGPoint point2 = [points[2] CGPointValue];
                    locResult->x3 = (int)point2.x;
                    locResult->y3 = (int)point2.y;
                    
                    CGPoint point3 = [points[3] CGPointValue];
                    locResult->x4 = (int)point3.x;
                    locResult->y4 = (int)point3.y;
                    
                    textResult->localizationResult = locResult;
                }
                
                
                memset(textResult->reserved, 0, sizeof(textResult->reserved));
                
                resultArray->results[i] = textResult;
            }
            
            *pResults = resultArray;
            
            return 0; 
        }
    }
    ```


- `void DBR_FreeTextResults(TextResultArray **pResults)`

    This method releases the memory allocated for the barcode results.
    
    ```objectivec
    void DBR_FreeTextResults(TextResultArray **pResults) {
        if (pResults == NULL || *pResults == NULL) return;
        
        TextResultArray *resultsArray = *pResults;
        
        for (int i = 0; i < resultsArray->resultsCount; i++) {
            TextResult *textResult = resultsArray->results[i];
            
            if (textResult->barcodeFormatString != NULL) {
                free(textResult->barcodeFormatString);
                textResult->barcodeFormatString = NULL;
            }
            
            if (textResult->barcodeText != NULL) {
                free(textResult->barcodeText);
                textResult->barcodeText = NULL;
            }
            
            if (textResult->localizationResult != NULL) {
                free(textResult->localizationResult);
                textResult->localizationResult = NULL;
            }
            
            free(textResult);
            resultsArray->results[i] = NULL;
        }
        
        if (resultsArray->results != NULL) {
            free(resultsArray->results);
            resultsArray->results = NULL;
        }
        
        free(resultsArray);
        *pResults = NULL;
    }
    ```

After successfully implementing the C functions, we can proceed to build the project, which results in generating the `bridge.framework`.

![Generated bridge.framework](https://www.dynamsoft.com/codepool/img/2024/04/build-ios-framework.jpg)

## Linking Third-Party iOS Frameworks to the Qt6 Project

We now have two frameworks: `DynamsoftBarcodeReader.framework` and `bridge.framework`. The next step involves linking them to our Qt6 project. To accomplish this, open the `CMakeLists.txt` file and incorporate the following lines specifically for the iOS platform:

```cmake
...
if(WIN32)
    ...
elseif(ANDROID)
    ...
elseif(APPLE)
    set_target_properties(${TARGET_NAME} PROPERTIES
        XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "iPhone Developer"
        MACOSX_BUNDLE TRUE
        XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/Frameworks"
    )

    target_link_libraries(${TARGET_NAME} PRIVATE "${PROJECT_SOURCE_DIR}/libs/ios/bridge.framework/bridge" PUBLIC "${PROJECT_SOURCE_DIR}/libs/ios/DynamsoftBarcodeReader.framework/DynamsoftBarcodeReader")

    # System frameworks
    find_library(UIKIT_FRAMEWORK UIKit)
    if(NOT UIKIT_FRAMEWORK)
        message(FATAL_ERROR "UIKit framework not found")
    endif()

    target_link_libraries(${TARGET_NAME} PRIVATE "${UIKIT_FRAMEWORK}")

    add_custom_command(TARGET ${TARGET_NAME} POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E copy_directory
        "${PROJECT_SOURCE_DIR}/libs/ios/DynamsoftBarcodeReader.framework"
        "$<TARGET_FILE_DIR:${TARGET_NAME}>/Frameworks/DynamsoftBarcodeReader.framework"
        COMMAND ls "$<TARGET_FILE_DIR:${TARGET_NAME}>/Frameworks/DynamsoftBarcodeReader.framework"
    )

    add_custom_command(TARGET ${TARGET_NAME} POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E copy_directory
        "${PROJECT_SOURCE_DIR}/libs/ios/bridge.framework"
        "$<TARGET_FILE_DIR:${TARGET_NAME}>/Frameworks/bridge.framework"
    )
endif()
...
```

The `target_link_libraries` function links both `bridge.framework` and `DynamsoftBarcodeReader.framework` to the Qt6 project. Furthermore, it's necessary to utilize the `add_custom_command` function to copy the frameworks into the `Frameworks` directory of the *.app package.

![iOS app structure](https://www.dynamsoft.com/codepool/img/2024/04/ios-app-structure.jpg)

For the application to run successfully on an actual iOS device, both frameworks must be signed with the same certificate as used in the Qt6 project. Otherwise, the app may encounter runtime crashes. Framework signing can be achieved using the `codesign` command:

1. Retrieve the signing identity:

    ```bash
    security find-identity -v -p codesigning
    ```

2. Proceed to sign the frameworks:

    ```bash
    /usr/bin/codesign --force --sign "IDENTITY" --timestamp=none --deep libs/ios/DynamsoftBarcodeReader.framework
    /usr/bin/codesign --force --sign "IDENTITY" --timestamp=none --deep libs/ios/bridge.framework
    ```

    Replace `IDENTITY` with your actual code-signing identity.

As an alternative to using the `codesign` and manual copy commands, we can open the Xcode project and add the frameworks to the `Frameworks, Libraries, and Embedded Content` section of the `General` tab. This method ensures the frameworks are automatically signed and copied during the project build in Xcode.

![Add frameworks to Xcode project](https://www.dynamsoft.com/codepool/img/2024/04/xcode-add-framework.jpg)

## Running the iOS Barcode and QR Code Scanner

Finally, we can deploy the Qt6 project to an iPhone or iPad device via Xcode or the Qt Creator.

![iOS barcode and QR code scanner](https://www.dynamsoft.com/codepool/img/2024/04/qt6-windows-qrcode-scanner.jpg)
 
## Source Code
[https://github.com/yushulx/Qt-QML-QR-code-scanner](https://github.com/yushulx/Qt-QML-QR-code-scanner)