Skip to content

New events handler

How events work libvlc side

Internally, libvlc sends events using the internal function libvlc_event_send on a libvlc_event_manager_t reference.
An iteration over an array of listeners created with libvlc_event_attach on the libvlc_event_manager_t is performed while a libvlc_event_manager_t::lock mutex is locked.
VLCKit objects attach themselves to various events on their initialisation phase.
VLCKit objects detach themselves from those events once the VLCKit object references count reach 0 and deallocation phase is triggered.
libvlc_event_manager_t::lock mutex is also locked when libvlc_event_detach is called.
This means detaching events always waits for any currently performed callback event.


Current issue

An event callback implementation in VLCKit can actually be illustrated like this :

static void AnyEventCallback(const libvlc_event_t * event, void* opaque) {
    @autoreleasepool {
        VLCKitObject *obj = (__bridge VLCKitObject *)opaque;
        dispatch_async(dispatch_get_main_queue(), ^{
            [obj doSomething];
        });
    }
}

But considering at the same time :

  • VLCKitObject *obj can be either in an allocated or deallocating state when bridge assignement is performed by VLCKitObject *obj = (__bridge VLCKitObject *)opaque
  • VLCKitObject *obj refcount can reach 0 from any other thread hence it's deallocation can be initiated from any other thread
  • VLCKitObject *obj reference is used asynchronously on main thread to send a doSomething message but VLCKitObject *obj can be in a deallocating state

So here we’re facing a situation where an EXC_BAD_ACCESS would happen once an attempt to perform any (__bridge MyObject *)opaque or send message on VLCKitObject *obj is done when it becomes an object in a deallocated/deallocating state.

We can then be tempted to try this strategy :

static void AnyEventCallback(const libvlc_event_t * event, void* opaque) {
    @autoreleasepool {
        __weak VLCKitObject *weak_obj = (__bridge VLCKitObject *)opaque;
        dispatch_async(dispatch_get_main_queue(), ^{
            __strong VLCKitObject *strong_obj = weak_obj;
            [strong_obj doSomething];
        });
    }
}

But once we run this, a SIGABRT would reach us with :

objc[10281]: Cannot form weak reference to instance (0x28266b040) of class VLCKitObject. It is possible that this object was over-released, or is in the process of deallocation.

So the idea of this patch is to provide a new mechanism to handle the events where _bridge cast of void *opaque never lead to an EXC_BAD_ACCESS.

By the way, another situation have to be considered.

To prevent this EXC_BAD_ACCESS issue, we have to prevent the VLCKitObject *obj to enter in a deallocating state from another thread.
The only option to achieve that is to find a way to keep a strong reference to the VLCKitObject *obj while we're using it in the event's callback's scope.
Hence given we enter a callback and the VLCKitObject *obj is still allocated, it makes sense that it would be possible that a deallocation will be triggered from the callback's calling thread.

Here's a reminder of the new problem we would get:

  • A VLCKitObject*, if not nil when assigned in a callback, will have to be strongly referenced to not be deallocated from another thread, hence it will be deallocated from the callback's calling thread if the refcount reaches 0 when going out of it's scope
  • A -[VLCKitObject dealloc] call will trigger libvlc_event_detach
  • Given a callback is called, the libvlc_event_manager_t::lock mutex is locked
  • Given an event is detached with libvlc_event_detach, the libvlc_event_manager_t::lock mutex is locked

It means in the callback calling thread we would potentially get this kind of call stack :
lock(mutex) => AnyEventCallback => -[VLCKitObject dealloc] => libvlc_event_detach => lock(mutex)

A recursive deadlock issue would be introduced if we keep a strong reference of a VLCKitObject* and allow its deallocation from any callback's calling thread.


New events handler proposal

The introduction of the new VLCEventsHandler object should perform a proper memory management which would help to to prevent any previously enumerated corner cases.

Any VLCKit object that observe libvlc events owns a strong reference to his VLCEventsHandler object and use it as a replacement for any callback user data, hence it's deallocated after all event callbacks are detached from vlc objects.

Memory management

This is how VLCKit object reference memory management is handled :

@interface VLCEventsHandler : NSObject
///[truncated]
@property (nonatomic, readonly, weak) id _Nullable object;
///[truncated]
@end
@implementation VLCEventsHandler {
///[truncated]
    /// Queue used to release asynchronously the retained object
    dispatch_queue_t _releaseQueue;
}
///[truncated]
- (void)handleEvent:(void (^)(id))handle {
    __block id object = _object; // We keep a strong reference and make it mutable in a block
    if (!object) { // We check if already deallocated
        return; // Object seems already deallocated, no need to handle the event
    }
    ///[truncated]
    dispatch_async(_releaseQueue, ^{
        object = nil; // We decrease object refcount in another thread, potentially triggering a deallocation if refcount reaches 0
    });
}
///[truncated]
@end

In the above code any VLCEventsHandler weakly references its related VLCKit object and pass it, if not nil, as strong reference to the block parameter in any -[VLCEventsHandler handleEvent:] call.
The VLCEventsHandler uses an internal queue to release the VLCKit object strong reference asynchronously, which helps to prevent the libvlc_event_manager_t mutex deadlock given a deallocation would happen on any callback calling thread.


Configuring events dispatch

With this proposal VLCKit will provide a new mechanism to configure events dispatch.

Any configuration can be provided with the use of the introduction of the VLCEventsConfiguring protocol:

@protocol VLCEventsConfiguring <NSObject>

- (dispatch_queue_t _Nullable) dispatchQueue;
- (BOOL) isAsync;

@end

Any configuration can be set globally with +[VLCLibrary sharedEventsConfiguration].

VLCEventsDefaultConfiguration is set when VLCLibrary class loads at runtime.

@implementation VLCEventsDefaultConfiguration


- (dispatch_queue_t _Nullable)dispatchQueue {
    return nil;
}

- (BOOL)isAsync {
    return NO;
}

@end

The above implementation tells the events to be dispatched synchronously from any callback's calling threads.

Another VLCEventsLegacyConfiguration is available if any client needs to use the former events dispatching mechanism.

@implementation VLCEventsLegacyConfiguration

- (dispatch_queue_t _Nullable)dispatchQueue {
    return dispatch_get_main_queue();
}

- (BOOL)isAsync {
    return YES;
}

@end

The above implementation tells the events to be dispatched asynchronously on the global main thread's queue.
This one would be of great help when we would like to migrate to VLCKit 4 when we're already using VLCKit 3.

The configuration will be used in -[VLCEventsHandler handleEvent:] implementation :

- (void)handleEvent:(void (^)(id))handle {
    ///[truncated]
    dispatch_block_t block = ^{handle(object);};
    if (_configuration.dispatchQueue) {
        if (_configuration.isAsync)
            dispatch_async(_configuration.dispatchQueue, block);
        else
            dispatch_sync(_configuration.dispatchQueue, block);
    } else {
        block();
    }
    ///[truncated]
}
Edited by Maxime Chapelet

Merge request reports