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.
This article is Part 2 in a 2-Part Series.
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
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.
The project structure is as follows:
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>
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 theDBRLicenseVerificationListener
protocol. TheDBRLicenseVerificationCallback
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 theNSArray<iTextResult*>*
to the C pointer. -
DBR_GetAllTextResults(void *barcodeReader, TextResultArray **pResults)
This method converts Objective-C
NSArray<iTextResult*>*
to CTextResultArray **
.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
.
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.
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:
-
Retrieve the signing identity:
security find-identity -v -p codesigning
-
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.
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.