Search code examples
grand-central-dispatchlibdispatch

What is a flexible method to convert a C style API to use libdispatch with blocks


I have an existing C API written in C that makes heavy use of status codes returned from functions for error handling. I'm trying to understand the preferred method of handling this type of situation with libdispatch.

Here's an example function. The RETURN_ERROR and related macros just call a function that returns an error code and prints a message.

int
mport_db_prepare(sqlite3 *db, sqlite3_stmt **stmt, const char * fmt, ...)
{
  va_list args;
  char *sql;

  va_start(args, fmt);
  sql = sqlite3_vmprintf(fmt, args);
  va_end(args);

  if (sql == NULL)
    RETURN_ERROR(MPORT_ERR_FATAL, "Couldn't allocate memory for sql statement");

  if (sqlite3_prepare_v2(db, sql, -1, stmt, NULL) != SQLITE_OK) {
    SET_ERRORX(MPORT_ERR_FATAL, "sql error preparing '%s': %s", sql, sqlite3_errmsg(db));
    sqlite3_free(sql);
    RETURN_CURRENT_ERROR;
  }

  sqlite3_free(sql);

  return MPORT_OK;
}

I'd like to use serial queues around the sql access bits and am aware I can use dispatch_sync to return values. However, I've also read that it's quite easy to get into deadlocks with many sync calls.

What would the best practice be for such an interface? Pass a block with a handler for success/fail? Would I provide a dispatch_queue_t parameter to let the completion block run on a user specified queue?


Solution

  • Here's a sample template you can use. This is my preferred approach. I'm typing this straight into the browser, so there may be some minor bugs (please edit):

    typedef void (^completion_block_t)(int error_code);
    
    static dispatch_once_t init_once;
    static dispatch_queue_t db_queue;
    
    void perform_init(void __unused *ctx) {
        db_queue = dispatch_queue_create("com.mycompany.db_queue", DISPATCH_QUEUE_SERIAL);
    }
    
    void perform_some_async_operation(dispatch_queue_t completion_queue, completion_block_t completion_block) {
        dispatch_once_f(&init_once, NULL, perform_init);
    
        if (completion_block) {
            completion_block = Block_copy(completion_block)
    
            if (!completion_queue) {
                completion_queue = dispatch_get_global_queue(qos_class_self(), 0);
            }
        }
    
        dispatch_async(db_queue, ^{
            int error_code = 0;
    
            ...
    
            if (completion_block) {
                dispatch_async(completion_queue, ^{
                    completion_block(error_code);
                });
            }
        });
    }
    

    Note that if you also want to provide a synchronous version of your API, it's best to implement your async version using it rather than the other way around. That way, the system can better keep track of dependencies for priority inheritance. Eg:

    int perform_some_sync_operation() {
        dispatch_once_f(&init_once, NULL, perform_init);
    
        __block int error_code = 0;
    
        dispatch_sync(db_queue, ^{
            ...
        });
    
        return error_code;
    }
    
    void perform_some_async_operation(dispatch_queue_t completion_queue, completion_block_t completion_block) {
        dispatch_queue_t execution_queue = dispatch_get_global_queue(qos_class_self(), 0);
    
        if (completion_block) {
            completion_block = Block_copy(completion_block)
    
            if (!completion_queue) {
                completion_queue = execution_queue;
            }
        }
    
        dispatch_async(execution_queue, ^{
            int error_code = perform_some_sync_operation();
    
            if (completion_block) {
                dispatch_async(completion_queue, ^{
                    completion_block(error_code);
                });
            }
        });
    }
    

    If your code is expected to run on systems older than Yosemite or iOS 8, you will need a fallback for when qos_class_self() is not available. Eg:

    completion_queue = dispatch_get_global_queue(&qos_class_self ? qos_class_self() : DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);