0

I have to use a built-in API that works like this

class MyAPIWrapper {
 func callAPI() {
 apiObj.delegate = self
 apiObj.doWork() // returns immediately, eventually calls self.delegate.workDone with the result on another thread
 }
 func workDone(result) {
 // do something with result
 }
}

You interact with the API by calling various methods on various API objects. Each method returns immediately and reports its success/failure by calling a method on the object's delegate once it is done.

The problem with this API is the delegate. Imagine if someone else calls doWork for that same object right after you do. They "steal" the delegate, and they will get both workDone callbacks (yours and theirs). Or even aside from that, it makes it very hard to call a sequence of methods on these API objects, because you have to implement manual synchronization mechanisms to make sure that you get a workDone call and the result was what you wanted before moving on to the next call.

Since I can't change this API, I want to encapsulate it to eliminate these problems. My idea was to essentially make the doWork call blocking. Then there's no delegate stealing and calling a sequence of methods is very easy. Something like this

class WrappedAPIObject
 init(apiObject) {
 self.apiObject = apiObject
 self.delegateMutex = Mutex()
 self.workCV = ConditionVariable()
 }
 func doWork() -> Result {
 delegateMutex.lock()
 apiObj.delegate = self
 apiObj.doWork()
 workCV.wait()
 retVal = self.result
 delegateMutex.unlock()
 return retVal
 }
 func workDone(result) {
 self.result = result
 workCV.notify()
 }
}

Then, as long as you create wrapped objects for each API object and always use those instead of the unwrapped objects, you don't have to worry about the delegates or asynchronous nature. If you want to call it asynchronously you can still spawn a thread like this

class APIWrapper
 func doLotsOfWork(wrappedObj) {
 Thread.new {
 result1 = wrappedObj.doWork()
 result2 = wrappedObj.doWork()
 }
 }
}

I've already tested this and it works great. But it makes me a little uneasy:

  1. The mutex/condition variable combo seems clunky. I feel like there's a more elegant pattern I'm missing here for simplifying this API.
  2. By forcing an async API to be synchronous, I force the consumer to spawn multiple threads to make it async again. In our case we have a lot of methods like doLotsOfWork that batch together API calls. If the user of our APIWrapper calls a whole bunch of these on the same wrapped object, it would spawn tons of threads which could only execute in serial, wasting system resources.

2 is my main concern. I feel like there should be a design where you can make multiple async calls to the same wrapped object and they all get queued up on one thread for that object.

Am I fretting over nothing? Or is there a way to fix the threading problem? Or is there a better way to avoid the delegate issue which I'm not seeing?

* If you care, the actual language is Swift, the lousy API is Apple's CoreBluetooth framework, the API objects are CBPeripherals, the "mutex" is a DispatchQueue and the "condition variable" is a DispatchGroup.

John Wu
27k10 gold badges69 silver badges93 bronze badges
asked Apr 26, 2019 at 22:02
6
  • 3
    Does the API allow you to instantiate more than one instance of apiObj at a time, or does all of your code have to share the one instance? Commented Apr 26, 2019 at 22:57
  • "Imagine if someone else calls doWork for that same object right after you do. They "steal" the delegate, and they will get both workDone callbacks" - if 2 request be started in parallel by same wrapper, and call the callback twice as they complete, is there a way to recognize from inside the callback to which request it relates? Commented Apr 27, 2019 at 5:11
  • what language is this? many have existing constructs like promises which do this kind of thing Commented Apr 27, 2019 at 6:26
  • 1
    helpful? learnappmaking.com/promises-swift-how-to Commented Apr 27, 2019 at 6:28
  • @JohnWu the API creates the objects internally and gives them to you. There's no way to request a distinct object to avoid the delegate issue Commented Apr 28, 2019 at 20:30

2 Answers 2

1

Imagine if someone else calls doWork for that same object right after you do. They "steal" the delegate, and they will get both workDone callbacks

basically, this is what you should be fixing first. As far as I understand from the API reference, you cannot uniquely identify requests so cannot freely run several at once, but you can run in parallel requests of different types, because they call different signatures of callback. Make a wrapper like this (I don't know Swift so making up syntax sometimes):

interface Request {
 func GetRequestData();
 func Callback(resultData);
}
class MyAPIWrapper {
 // this has linear cost for operations, if it is an issue a dictionary of something can be used.
 List<Request> requestsInProgress;
 ApiObj apiObj;
 func Initialize() {
 apiObj.delegate = self
 }
 func callAPI(request) {
 if(TryRegisterRequest(request)) {
 apiObj.doWork(request.GetRequestData());
 } else {
 // caller error: another request is already running which would be indistinguihable
 }
 }
 func workDone(result) {
 Request? request = TryFindAndUnregisterRequest(result);
 if(request != nil) {
 request!.Callback(result);
 } else {
 // internal error: unexpected result
 }
 }
 func TryRegisterRequest(Request request) {
 for (Request r in requestsInProgress) {
 if (....) { // chacks if r matches request
 return false;
 }
 }
 requestsInProgress.Add(request);
 }
 func TryFindAndUnregisterRequest(result) {
 for (Request r in requestsInProgress) {
 if (....) { // checks if r matches result
 requestsInProgress.Remove(r);
 return r; // is this how I construct a filled optional?
 }
 }
 // not found
 return nil;
 }
}

This wrapper provides intercface without the quoted issue. Then, you can make a synchronous wrapper over it. Or not.

answered Apr 27, 2019 at 5:39
-2

You are talking about CoreBlueTooth. I can't see any reason why you would ever change the delegate.

answered Apr 27, 2019 at 9:41
2
  • ??? If you pass around CBPeripheral objects there's literally no built-in way to tell if its delegate is still waiting for a callback and thus if it's safe to do any read/writes with it Commented Apr 28, 2019 at 20:29
  • Perhaps I completely misunderstood how you're supposed to interact with CoreBluetooth. Is the intention that I should only have a single object interact with all BLE devices (thus all peripherals have the same delegate)? Commented Apr 29, 2019 at 14:28

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.