Skip to content

Conversation

@JaviSoto
Copy link

@JaviSoto JaviSoto commented Feb 4, 2025

No description provided.

Comment on lines -49 to -50
objc_sync_enter(self)
defer { objc_sync_exit(self) }
Copy link
Author

@JaviSoto JaviSoto Feb 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's safe to use this API from Swift, especially when combined with other primitives which callers of this API may be using. Since I had to make other mutable properties thread-safe, I just used the same @Synchronized primitive for all of them

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interesting, i'd be curious to see any resources you have indicating it's unsafe, although using a lock does seem simpler

}

public final class ZippyJSONDecoder {
public final class ZippyJSONDecoder: Sendable {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This compiles without having to resort to @unchecked Sendable, so we get a compile-time guarantee of thread-safety :)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very nice!

case deferredToData
case base64
case custom((Decoder) throws -> Data)
case custom(@Sendable (Decoder) throws -> Data)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this appears to be a breaking change, but maybe it's worth it

}
}

private final class Lock: LockProtocol {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we could remove this class and just have a free function that returns a LockProtocol


@propertyWrapper
private final class Synchronized<Value>: @unchecked Sendable {
private let lock: LockProtocol
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so it appears that this design creates like 4 of these per decoder? i believe that should be fast, but i may verify

public var nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy
@Synchronized
private var _nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy
public var nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why can't public var have @Synchronized on it and we don't need to wrap? is it to avoid exposing @Synchronized?

@michaeleisel
Copy link
Owner

thank you for this pr. i should mention that JSONDecoder is superior to ZippyJSON for iOS 17+, as discussed here: #69 . things may get archived in, say, a year. so if you're doing this change in part for your own code base, i'm curious to know if it supports ios 16 or prior

although this pr makes sense with what it's trying to do, i have a high level concern. if someone, say, changes the data decoding strategy of a decoder on thread A while thread B is doing json decoding with it, then although the changeover will be safe, the results are probably undesired. data decoding would behave differently for the first part of the json document than the second. i don't know what the best solution is here, but i'm not sure if people should actually be using it from multiple threads here (it should be noted i haven't done a ton with swift since Sendable came out)

@JaviSoto
Copy link
Author

JaviSoto commented Feb 5, 2025

thank you for this pr. i should mention that JSONDecoder is superior to ZippyJSON for iOS 17+, as discussed here: #69 . things may get archived in, say, a year. so if you're doing this change in part for your own code base, i'm curious to know if it supports ios 16 or prior

Hey! Thanks for that :) I was actually just in the process of doing benchmarks in my app. But that is good to hear! May be worth updating the README :)

changes the data decoding strategy of a decoder on thread A while thread B is doing json decoding with it, then although the changeover will be safe, the results are probably undesired. data decoding would behave differently for the first part of the json document than the second. i don't know what the best solution is here, but i'm not sure if people should actually be using it from multiple threads here (it should be noted i haven't done a ton with swift since Sendable came out)

yeah totally. I wonder what JSONDecoder does. It's marked @unchecked Sendable. To me it would make more sense as a struct which would avoid that issue altogether.

I'm happy to just close this PR though!

@michaeleisel
Copy link
Owner

Hey! Thanks for that :) I was actually just in the process of doing benchmarks in my app. But that is good to hear! May be worth updating the README :)

Sure, that's a good idea, I'll update it.

yeah totally. I wonder what JSONDecoder does. It's marked @unchecked Sendable. To me it would make more sense as a struct which would avoid that issue altogether.

It looks like this:

    open var nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy {
        get {
            optionsLock.lock()
            defer { optionsLock.unlock() }
            return options.nonConformingFloatDecodingStrategy
        }
        _modify {
            optionsLock.lock()
            var value = options.nonConformingFloatDecodingStrategy
            defer {
                options.nonConformingFloatDecodingStrategy = value
                optionsLock.unlock()
            }
            yield &value
        }
        set {
            optionsLock.lock()
            defer { optionsLock.unlock() }
            options.nonConformingFloatDecodingStrategy = newValue
        }
    }

So I guess they use a shared lock. It has that fancy _modify which I guess is after my time. They're modifying a struct options, and based on JSONDecoderImpl(userInfo: self.userInfo, from: map, codingPathNode: .root, options: self.options), they keep a separate struct for each parsing. So it looks like they've solved the higher level issue.

If you want to solve the high level problem and then mark it Sendable, or if you want to merge your current changes minus marking it Sendable, or of course abandon the PR, all are things I'm on board with

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.

2 participants