Skip to content

Swift: new wrapper based on C-API instead of ObjectiveC++ - #1061

Open
axxel wants to merge 1 commit into
zxing-cpp:masterfrom
axxel:swift-wrapper
Open

Swift: new wrapper based on C-API instead of ObjectiveC++#1061
axxel wants to merge 1 commit into
zxing-cpp:masterfrom
axxel:swift-wrapper

Conversation

@axxel

@axxel axxel commented Feb 24, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

@axxel
axxel force-pushed the swift-wrapper branch 2 times, most recently from 90c3e9b to 60646f2 Compare February 24, 2026 18:58
axxel referenced this pull request in MarvinFrankenfeld/zxing-cpp Feb 24, 2026
@axxel axxel changed the title swift: new wrapper based on C-API instead of ObjectiveC++ Swift: new wrapper based on C-API instead of ObjectiveC++ Feb 24, 2026
@farfromrefug

farfromrefug commented Feb 26, 2026

Copy link
Copy Markdown

@axxel does that mean you are dropping the objc version? if yes then it would be nice for the swift version to support @objc. many projects still use objc and won't stop anytime soon (much better c++ interoperability).
we also use it in an environment where we need objc.

@axxel
axxel force-pushed the swift-wrapper branch 2 times, most recently from 77312a8 to c6ec5ff Compare February 26, 2026 23:18
@axxel

axxel commented Feb 26, 2026

Copy link
Copy Markdown
Collaborator Author

does that mean you are dropping the objc version?

Not right away, if I can help it (see #726 (comment)). But if you want access to the libzint based backend, then you would (as of today) need to use the Swift based wrapper.

@jknlsn

jknlsn commented Mar 14, 2026

Copy link
Copy Markdown

I tested this wrapper over the last couple of weeks and it works very well. Since it hasn’t landed yet, I’d consider optimising for the most idiomatic Swift API shape now rather than carrying through rough edges from the native/C surface.

From a Swift API design perspective, I think a few changes would make it feel much more natural and safer for Apple-platform use:

  • Use value-typed configuration (WriterOptions, BarcodeReader.Configuration) so config is naturally Sendable with no shared mutable state. Build/apply C option handles per call (read / toImage / toSVG) instead of retaining mutable native option handles across object lifetime.
  • Prefer Swift value semantics for wrapper-facing models as much as possible, especially Barcode / decoded results. Swift callers will expect equality, hashing, copying, and concurrency behavior to reflect barcode content/state, not native handle identity.
  • Throw from read(from:) when ZXing_ReadBarcodes returns nil, rather than returning []. A nil there indicates a bridge-level failure; collapsing it to “no results” makes real errors silent and much harder to diagnose.
  • Fix pointer lifetime in ImageView(data:): don’t store a pointer obtained from withUnsafeBytes beyond the closure scope. Retain stable backing storage instead and use that pointer.
  • Keep native/C semantics in the native layer, and handle Swift policy in the Swift bridge layer. For example, preserve the raw C API behavior for empty formats, and make the Swift wrapper decide how “empty means default/all” should behave.
  • Avoid trap-based enum/raw-value bridging. If native/C returns an unknown enum value, the Swift wrapper should fail gracefully (or throw on throwing boundaries) rather than crashing the app.

More broadly, I think the wrapper should lean Swift-first where it can by using typed APIs and value semantics by default, with low-level/raw escape hatches still available where the native surface is broader. I made a broader Swift-focused pass after this revision covering the items above plus some docs/tests/ergonomics cleanup. I’ve pushed a concrete implementation for reference here: swift-wrapper. Two commits so that a little easier to follow, logic in the first and then the second modularises into separate files.

This doesn’t cover the Objective-C wrapping discussed in this comment, as I haven’t worked with Objective-C / CocoaPods myself. With that caveat, the current and proposed approach both sound reasonable to me.

To sum up: I don’t think any of this is fundamentally wrong in its current shape, but I do think these proposed changes would make the wrapper significantly more “Swifty” before it lands!

Thanks for kicking this all off @axxel, really fantastic and greatly appreciated.

@axxel

axxel commented Mar 14, 2026

Copy link
Copy Markdown
Collaborator Author

First of all: thank you very much for your detailed and in-depth feedback and actual proposal to look at. I wrote like 0 lines of swift in my life, so feedback from someone actually at home in this domain is very much appreciated.

That said, there is a lot of stuff I like, a lot of stuff I don't like and a bunch of other stuff that I would need to be convinced of, yet. It'll take some iteration to go over those in a comprehensive manner. I'll just start somewhere...

From a Swift API design perspective, I think a few changes would make it feel much more natural and safer for Apple-platform use:

  • Use value-typed configuration (WriterOptions, BarcodeReader.Configuration) so config is naturally Sendable with no shared mutable state. Build/apply C option handles per call (read / toImage / toSVG) instead of retaining mutable native option handles across object lifetime.

I deliberately did not do that since I wanted to prevent 2 things: a) copying (meaning maintaining duplicates) or loosing default values and b) having to set all values, even when they are not changed. Loosing those features might be worth it if making them Sendable is outweighing their benefits. What is the use case that your approach supports over mine?

  • Prefer Swift value semantics for wrapper-facing models as much as possible, especially Barcode / decoded results. Swift callers will expect equality, hashing, copying, and concurrency behavior to reflect barcode content/state, not native handle identity.

You realized that keeping the Barcode handle is required for the zint machinery. I have not understood what your approach brings to the table then. I understand that you might want to determine equality of 2 barcodes. There needs to be a distinction made between reading and writing in this context.

reading: The library has an internal concept of two barcodes being "euqual", that means two peaces of (linear) barcode or two detections from different resolutions are referring to the same symbol in the image. The idea is that every Barcode object returned from read is by definition different from every other one (if not, then there is a bug in the c++ code). So checking for native handle equality is the correct approach here (although not very useful).

writing: The creation part is modeled as a Barcode object in v3.0 for the first time and admittedly the equality property has not been updated or re-evaluated for that, yet. I would see a point in arguing that two barcodes that are created from the exact same input to CreateBarcodeFromText() can or should be considered equal. But then again, I question the value of implementing that. What would be the use case?

Also, there is one expensive property (symbol), which might actually be missing in the C-API right now. That hardly anyone will need. Moving that over to a Swift value is therefore a pessimization. And there is the extra(key) feature that also needs the handle.

  • Throw from read(from:) when ZXing_ReadBarcodes returns nil, rather than returning []. A nil there indicates a bridge-level failure; collapsing it to “no results” makes real errors silent and much harder to diagnose.

Good catch, even though the ZXing_ReadBarcodes function only ever fails if you pass it invalid parameters.

  • Fix pointer lifetime in ImageView(data:): don’t store a pointer obtained from withUnsafeBytes beyond the closure scope. Retain stable backing storage instead and use that pointer.

The retained object is the same (e.g. data) so what is wrong with the old code?

  • Keep native/C semantics in the native layer, and handle Swift policy in the Swift bridge layer. For example, preserve the raw C API behavior for empty formats, and make the Swift wrapper decide how “empty means default/all” should behave.

Looks like I have missed that part, I'm running low on time right now, so I'm skipping that.

  • Avoid trap-based enum/raw-value bridging. If native/C returns an unknown enum value, the Swift wrapper should fail gracefully (or throw on throwing boundaries) rather than crashing the app.

Not quite clear where you see the source of those drifting apart.

More broadly, I think the wrapper should lean Swift-first where it can

Totally agree.

I’ve pushed a concrete implementation for reference here: swift-wrapper.

Really appreciated. Before getting into nighty gritty details, here are some top-level thoughts:

  1. I generally don't want to change the C-API. The CharacterSet stuff is something that I'd prefer to see gone in the C++-API, therefore I never added it to the C-API in the first place. The .._cropped() and ..._rotated() additions might be worth adding but that could be a different discussion.
  2. I generally want to keep the symbol naming consistent across the whole project, e.g. addHRT, BarcodeLists.list(), Barcode.extra() (there is a whole discussion about that particular term and why I chose it over 'metadata' somewhere in the depth of the GitHub project ;)).
  3. I generally prefer the code to be minimal, things like your isKnown / resolvedName properties seem questionable to me (not clean what you're trying to achieve here).
  4. Normalized/rotated UIImages sound very good. But there might be a caveat: the position of the barcodes returned then changes their meaning. That might exactly what one expects or it might not.
  5. Why do you think BarcodeFormat.formats() is a good idea?
  6. Why did you introduce BytesPerPixel? Note that for subsampled ImageView data, that is simply wrong.
  7. why did you change the Position.String() implementation?
  8. The Symbology specific creator options might be a good idea but they might also be unnecessarily bloated. If they should be worth it, then that would be non-Swift specific and I'd like to address that at a later time.
  9. I don't like this subfolder structure in your second commit. I decided to split the wrapper in 2 files as that seemed most consistent with the others. That said, the new go wrapper is split into more parts. Once the important questions are settled, I might revisit this subject.

To sum up: I don’t think any of this is fundamentally wrong in its current shape, but I do think these proposed changes would make the wrapper significantly more “Swifty” before it lands!

100% my intention! :)

Not quite sure how to efficiently move forward from here. The best I can currently think is: you could address my questions above, maybe change your code where there is no open question left and then it might make most sense for me to merge my code and then let you propose PRs (that are reasonably compartmentalized), which would allow to properly address individual parts with comments while making it sure that your contributions stay properly attributed in the end. Does that make sense to you?

@jknlsn

jknlsn commented Mar 15, 2026

Copy link
Copy Markdown

Thanks for the reply and the feedback! Some of this may well be me lacking project/C++ context, so I very much appreciate the clarifications.

I deliberately did not do that since I wanted to prevent 2 things: a) copying (meaning maintaining duplicates) or loosing default values and b) having to set all values, even when they are not changed. Loosing those features might be worth it if making them Sendable is outweighing their benefits. What is the use case that your approach supports over mine?

Those two concerns are still covered in my implementation, cDefaults preserves the C library's defaults, and the Swift initialiser only requires callers to set the values they want to change. The difference is just where the mutable C state lives, temporarily at the call boundary rather than permanently inside the Swift object, because I didn't see a need to keep that native mutable state around between calls. So I don't think those features need to be lost, and the main additional benefit is compiler-checked Sendable conformance. In my own projects I'm trying to use Swift 6 strict concurrency where possible, so I do see value in the compiler-enforced concurrency safety that comes from that.

You realized that keeping the Barcode handle is required for the zint machinery. I have not understood what your approach brings to the table then.

The main thing the struct approach brings is a Swift value-type surface with Sendable behaviour at the wrapper boundary, rather than exposing a mutable reference type with native state directly to callers. The handle is still retained internally for rendering and metadata access, so nothing is lost on that front. On the equality question I'm happy to leave content-based equality out of the discussion for now. You're right that the use case is less clear-cut, and callers who need to de-duplicate can always compare fields directly rather than having that baked into the wrapper.

Also, there is one expensive property (symbol)... Moving that over to a Swift value is therefore a pessimization.

Agreed, if symbol were exposed, it should definitely be lazy/on-demand rather than eagerly copied. That could be addressed by trimming the eager-copy set to only the commonly-accessed fields.

The retained object is the same (e.g. data) so what is wrong with the old code?

The concern is less about whether it happens to work in practice, and more about what lifetime the API actually guarantees. withUnsafeBytes documents the buffer as only valid for the duration of the closure, so storing that pointer in the native ImageView means the pointer escapes the scope Swift guarantees. By contrast, NSData.bytes gives a pointer whose lifetime is tied to the retained NSData object. Since the wrapper already retains that object for the lifetime of the ImageView, using .bytes makes the pointer lifetime explicit and guaranteed by the API contract.

Looks like I have missed that part, I'm running low on time right now, so I'm skipping that.

No problem. On another look I don't think this one is worth changing. The current behavior is fine as-is.

Not quite clear where you see the source of those drifting apart.

The main thing I was thinking about is that the enums are manually mirrored between C++ and Swift with no codegen keeping them in sync. So if a case gets added on the C++ side and the Swift wrapper doesn't get updated at the same time, it would still compile fine and the crash would only show up at runtime when a user happens to hit the new value. It just feels more natural to me for the bridging to be defensive there, since a thrown error is a lot more actionable than a bare force-unwrap crash. And the code cost is slim, it's basically try instead of ! at the call sites.

I tightened this into a single commit with just the points above implemented for reference: f0c3f7e.

On your numbered points about the reference implementation (naming consistency, file structure, C API changes, etc.), those are all entirely fair, and I think separate focused PRs would be a better way to discuss the ones worth pursuing.

Overall, I agree that the best option is to land this PR and then look at further changes from there. The current implementation already looks entirely usable from Swift/iOS to me, so none of my suggestions are blockers :)

@axxel

axxel commented Mar 16, 2026

Copy link
Copy Markdown
Collaborator Author

Those two concerns are still covered in my implementation.

Not the two I had in my mind:

  1. you provided the c++ defaults as a fallback for when the ZXing_..._new() call failed. Then I see no point in querying them during startup at all.
  2. I was referring to having to call all individual setter functions even when the new value is the same as the old.

If being Sendable is a priority, then I'd suggest to either make them a trivial struct with trivial hard-coded initialization (would allow to query those default values from client code) or to make the struct contain nullable versions of all parameters and then you'd only need to set the values that are specified and would not need to duplicate the default value definition.

A little ChatGPT chat also made aware of the concept of an actor in Swift which is supposedly used inside Apple Frameworks in such a scenario:

actor DecoderCore {
    private let ptr: OpaquePointer

    init() {
        ptr = ZXing_CreateDecoder()
    }

    deinit {
        ZXing_DestroyDecoder(ptr)
    }

    func decode(_ image: ImageData) -> DecodeResult {
        ZXing_Decode(ptr, image.buffer, image.width, image.height)
    }
}

public struct BarcodeReader {
    private let core: DecoderCore

    public init() {
        core = DecoderCore()
    }

    public func decode(_ image: ImageData) async -> DecodeResult {
        await core.decode(image)
    }
}

But that clearly looks like overkill to me, just to assemble and pass some parameters.

But my question about what it is that you actually gain by making them sendable is still open. Can you describe a typical use case that would benefit from that feature? Maybe it would be obvious to me if I ever wrote an app in Swift... ;).

From a high level perspective, all I want is to provide a single function readBarcodes() with a bunch of optional parameters where I want to specify only those that I care about. In contrast to C++, Swift actually provides a means to achieve that directly. Then the question of how you would transfer those parameters from one thread to another is simply out of scope of the wrapper.

I have not understood what your approach brings to the table then.

The main thing the struct approach brings is a Swift value-type surface with Sendable behaviour at the wrapper boundary, rather than exposing a mutable reference type with native state directly to callers. The handle is still retained internally for rendering and metadata access, so nothing is lost on that front.

From what I just learned about @unchecked Sendable, that is an appropriate annotation for the current Barcode class as is and supports all your use cases, right? Any missing thread locking (regarding the zint structure during WriteBarcodeTo...()) calls should be added inside the core. And I believe you were right to point those out. Thanks.

The retained object is the same (e.g. data) so what is wrong with the old code?

The concern is less about whether it happens to work in practice, and more about what lifetime the API actually guarantees.

Thanks. Understood.

No problem. On another look I don't think this one is worth changing. The current behavior is fine as-is.

Very well.

Not quite clear where you see the source of those drifting apart.

The main thing I was thinking about is that the enums are manually mirrored between C++ and Swift with no codegen keeping them in sync. So if a case gets added on the C++ side and the Swift wrapper doesn't get updated at the same time, it would still compile fine and the crash would only show up at runtime when a user happens to hit the new value. It just feels more natural to me for the bridging to be defensive there, since a thrown error is a lot more actionable than a bare force-unwrap crash. And the code cost is slim, it's basically try instead of ! at the call sites.

That can only happen for returned enum types. Those are ContentType and BarcodeFormat. The latter is actually generated from the BarcodeFormat.h definition (but admittedly not automatically on every build), the former will hardly ever change. I would not consider this an issue worth changing the API for.

Overall, I agree that the best option is to land this PR and then look at further changes from there. The current implementation already looks entirely usable from Swift/iOS to me, so none of my suggestions are blockers :)

All right. Will do then. Things that I like to see merged are:

  1. fix the read function and fix for the raw pointer in ImageView
  2. the automatic UiImage -> CGImage` orientation adjustment, if this is really what makes it usable more intuitively.

@axxel

axxel commented Mar 16, 2026

Copy link
Copy Markdown
Collaborator Author

I pushed a new commit that fixes a merge conflict, adds the @unchecked Sendable to Barcode, splits the BarcodeFormat constants into a generatable separate file. (I believe that was it).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants