Integrating Third-Party iOS Frameworks into a Qt6 Barcode and QR Code Scanner

In a previous article, we developed a barcode and QR code scanner 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

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:

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

exported symbols of Dynamsoft iOS Barcode SDK

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

The project structure is as follows:

iOS Framework project structure

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.

#import <DynamsoftBarcodeReader/DynamsoftBarcodeReader.h>

Add search path for header files

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

#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.

      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.

      @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.

      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.

      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.

      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.

      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 **.

      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.

      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

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:

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

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:

     security find-identity -v -p codesigning
    
  2. Proceed to sign the frameworks:

     /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

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

Source Code

https://github.com/yushulx/Qt-QML-QR-code-scanner