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.

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?
    – John Wu
    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?
    – max630
    Commented Apr 27, 2019 at 5:11
  • what language is this? many have existing constructs like promises which do this kind of thing
    – Ewan
    Commented Apr 27, 2019 at 6:26
  • 1
    helpful? learnappmaking.com/promises-swift-how-to
    – Ewan
    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
    – Max
    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.

-2

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

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
    – Max
    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)?
    – Max
    Commented Apr 29, 2019 at 14:28

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.