core: Refactor initialization and how the default context is handled

Highlights for this change:

 - usbi_default_context is only set if libusb_init() is called with NULL.
 - All hotplug related functionality (e.g. initialization, processing) has been
   moved to hotplug.c
 - Backends are simplified by removing initialization mutexes. Mutual exclusion
   between init()/exit() is provided by default_context_lock.
 - Make hotplug types and functions part of libusbi.h with the common usbi_
   prefixes (removes hotplug.h).

Addresses issue highlighted in #855

Closes #856

Signed-off-by: Chris Dickens <christopher.a.dickens@gmail.com>
Signed-off-by: Nathan Hjelm <hjelmn@google.com>
diff --git a/Xcode/libusb.xcodeproj/project.pbxproj b/Xcode/libusb.xcodeproj/project.pbxproj
index 958e256..759a102 100644
--- a/Xcode/libusb.xcodeproj/project.pbxproj
+++ b/Xcode/libusb.xcodeproj/project.pbxproj
@@ -56,7 +56,6 @@
 		008FC0211628BC5200BC5BE2 /* ezusb.c in Sources */ = {isa = PBXBuildFile; fileRef = 008FBFDC1628BA0E00BC5BE2 /* ezusb.c */; };
 		008FC0301628BC7400BC5BE2 /* listdevs.c in Sources */ = {isa = PBXBuildFile; fileRef = 008FBFE71628BA0E00BC5BE2 /* listdevs.c */; };
 		1438D77A17A2ED9F00166101 /* hotplug.c in Sources */ = {isa = PBXBuildFile; fileRef = 1438D77817A2ED9F00166101 /* hotplug.c */; };
-		1438D77B17A2ED9F00166101 /* hotplug.h in Headers */ = {isa = PBXBuildFile; fileRef = 1438D77917A2ED9F00166101 /* hotplug.h */; };
 		1438D77F17A2F0EA00166101 /* strerror.c in Sources */ = {isa = PBXBuildFile; fileRef = 1438D77E17A2F0EA00166101 /* strerror.c */; };
 		2018D95F24E453BA001589B2 /* events_posix.c in Sources */ = {isa = PBXBuildFile; fileRef = 2018D95E24E453BA001589B2 /* events_posix.c */; };
 		2018D96124E453D0001589B2 /* events_posix.h in Headers */ = {isa = PBXBuildFile; fileRef = 2018D96024E453D0001589B2 /* events_posix.h */; };
@@ -266,7 +265,6 @@
 		008FC0151628BC0300BC5BE2 /* fxload */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = fxload; sourceTree = BUILT_PRODUCTS_DIR; };
 		008FC0261628BC6B00BC5BE2 /* listdevs */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = listdevs; sourceTree = BUILT_PRODUCTS_DIR; };
 		1438D77817A2ED9F00166101 /* hotplug.c */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.c.c; path = hotplug.c; sourceTree = "<group>"; tabWidth = 4; usesTabs = 1; };
-		1438D77917A2ED9F00166101 /* hotplug.h */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.c.h; path = hotplug.h; sourceTree = "<group>"; tabWidth = 4; usesTabs = 1; };
 		1438D77E17A2F0EA00166101 /* strerror.c */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.c.c; path = strerror.c; sourceTree = "<group>"; tabWidth = 4; usesTabs = 1; };
 		1443EE8416417E63007E0579 /* common.xcconfig */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = text.xcconfig; path = common.xcconfig; sourceTree = SOURCE_ROOT; tabWidth = 4; usesTabs = 1; };
 		1443EE8516417E63007E0579 /* debug.xcconfig */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = text.xcconfig; path = debug.xcconfig; sourceTree = SOURCE_ROOT; tabWidth = 4; usesTabs = 1; };
@@ -417,7 +415,6 @@
 				008FBF541628B7E800BC5BE2 /* core.c */,
 				008FBF551628B7E800BC5BE2 /* descriptor.c */,
 				1438D77817A2ED9F00166101 /* hotplug.c */,
-				1438D77917A2ED9F00166101 /* hotplug.h */,
 				008FBF561628B7E800BC5BE2 /* io.c */,
 				008FBF5A1628B7E800BC5BE2 /* libusb.h */,
 				008FBF671628B7E800BC5BE2 /* libusbi.h */,
@@ -498,7 +495,6 @@
 				008FBFA51628B84200BC5BE2 /* config.h in Headers */,
 				008FBF931628B7E800BC5BE2 /* darwin_usb.h in Headers */,
 				2018D96124E453D0001589B2 /* events_posix.h in Headers */,
-				1438D77B17A2ED9F00166101 /* hotplug.h in Headers */,
 				008FBF901628B7E800BC5BE2 /* libusbi.h in Headers */,
 				008FBF9B1628B7E800BC5BE2 /* threads_posix.h in Headers */,
 				008FBFA11628B7E800BC5BE2 /* version.h in Headers */,
diff --git a/doc/Makefile.in b/doc/Makefile.in
index 45c3209..6568c4f 100644
--- a/doc/Makefile.in
+++ b/doc/Makefile.in
@@ -1,5 +1,5 @@
 LIBUSB_SRC_DIR = @top_srcdir@/libusb
-EXCLUDED_FILES = hotplug.h libusbi.h version.h version_nano.h
+EXCLUDED_FILES = libusbi.h version.h version_nano.h
 LIBUSB_SRC = $(wildcard $(LIBUSB_SRC_DIR)/*.c) $(wildcard $(LIBUSB_SRC_DIR)/*.h)
 LIBUSB_DOC_SRC = $(filter-out $(addprefix $(LIBUSB_SRC_DIR)/,$(EXCLUDED_FILES)),$(LIBUSB_SRC))
 
diff --git a/doc/doxygen.cfg.in b/doc/doxygen.cfg.in
index 10f8fc4..b6e6219 100644
--- a/doc/doxygen.cfg.in
+++ b/doc/doxygen.cfg.in
@@ -899,8 +899,7 @@
 # Note that relative paths are relative to the directory from which doxygen is
 # run.
 
-EXCLUDE                = @top_srcdir@/libusb/hotplug.h \
-                         @top_srcdir@/libusb/libusbi.h \
+EXCLUDE                = @top_srcdir@/libusb/libusbi.h \
                          @top_srcdir@/libusb/version.h \
                          @top_srcdir@/libusb/version_nano.h \
                          @top_srcdir@/libusb/os
diff --git a/libusb/Makefile.am b/libusb/Makefile.am
index c78006e..baf7b38 100644
--- a/libusb/Makefile.am
+++ b/libusb/Makefile.am
@@ -82,7 +82,7 @@
 
 libusb_1_0_la_LDFLAGS = $(LT_LDFLAGS)
 libusb_1_0_la_SOURCES = libusbi.h version.h version_nano.h \
-	core.c descriptor.c hotplug.h hotplug.c io.c strerror.c sync.c \
+	core.c descriptor.c hotplug.c io.c strerror.c sync.c \
 	$(PLATFORM_SRC) $(OS_SRC)
 
 pkginclude_HEADERS = libusb.h
diff --git a/libusb/core.c b/libusb/core.c
index 7a2f6c3..8414756 100644
--- a/libusb/core.c
+++ b/libusb/core.c
@@ -21,7 +21,6 @@
  */
 
 #include "libusbi.h"
-#include "hotplug.h"
 #include "version.h"
 
 #ifdef __ANDROID__
@@ -33,17 +32,18 @@
 #include <syslog.h>
 #endif
 
-struct libusb_context *usbi_default_context;
 static const struct libusb_version libusb_version_internal =
 	{ LIBUSB_MAJOR, LIBUSB_MINOR, LIBUSB_MICRO, LIBUSB_NANO,
 	  LIBUSB_RC, "http://libusb.info" };
-static int default_context_refcnt;
-static usbi_mutex_static_t default_context_lock = USBI_MUTEX_INITIALIZER;
 static struct timespec timestamp_origin;
 #if defined(ENABLE_LOGGING) && !defined(USE_SYSTEM_LOGGING_FACILITY)
 static libusb_log_cb log_handler;
 #endif
 
+struct libusb_context *usbi_default_context;
+static int default_context_refcnt;
+static usbi_mutex_static_t default_context_lock = USBI_MUTEX_INITIALIZER;
+
 usbi_mutex_static_t active_contexts_lock = USBI_MUTEX_INITIALIZER;
 struct list_head active_contexts_list;
 
@@ -710,9 +710,8 @@
 	dev->session_data = session_id;
 	dev->speed = LIBUSB_SPEED_UNKNOWN;
 
-	if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) {
-		usbi_connect_device (dev);
-	}
+	if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG))
+		usbi_connect_device(dev);
 
 	return dev;
 }
@@ -727,12 +726,7 @@
 	list_add(&dev->list, &dev->ctx->usb_devs);
 	usbi_mutex_unlock(&dev->ctx->usb_devs_lock);
 
-	/* Signal that an event has occurred for this device if we support hotplug AND
-	 * the hotplug message list is ready. This prevents an event from getting raised
-	 * during initial enumeration. */
-	if (libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG) && dev->ctx->hotplug_msgs.next) {
-		usbi_hotplug_notification(ctx, dev, LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED);
-	}
+	usbi_hotplug_notification(ctx, dev, LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED);
 }
 
 void usbi_disconnect_device(struct libusb_device *dev)
@@ -745,13 +739,7 @@
 	list_del(&dev->list);
 	usbi_mutex_unlock(&ctx->usb_devs_lock);
 
-	/* Signal that an event has occurred for this device if we support hotplug AND
-	 * the hotplug message list is ready. This prevents an event from getting raised
-	 * during initial enumeration. libusb_handle_events will take care of dereferencing
-	 * the device. */
-	if (libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG) && dev->ctx->hotplug_msgs.next) {
-		usbi_hotplug_notification(ctx, dev, LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT);
-	}
+	usbi_hotplug_notification(ctx, dev, LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT);
 }
 
 /* Perform some final sanity checks on a newly discovered device. If this
@@ -2247,113 +2235,105 @@
  * context will be created. If there was already a default context, it will
  * be reused (and nothing will be initialized/reinitialized).
  *
- * \param context Optional output location for context pointer.
+ * \param ctx Optional output location for context pointer.
  * Only valid on return code 0.
  * \returns 0 on success, or a LIBUSB_ERROR code on failure
  * \see libusb_contexts
  */
-int API_EXPORTED libusb_init(libusb_context **context)
+int API_EXPORTED libusb_init(libusb_context **ctx)
 {
-	struct libusb_device *dev, *next;
 	size_t priv_size = usbi_backend.context_priv_size;
-	struct libusb_context *ctx;
-	static int first_init = 1;
-	int r = 0;
+	struct libusb_context *_ctx;
+	int r;
 
 	usbi_mutex_static_lock(&default_context_lock);
 
-	if (!timestamp_origin.tv_sec)
-		usbi_get_monotonic_time(&timestamp_origin);
-
-	if (!context && usbi_default_context) {
+	if (!ctx && usbi_default_context) {
 		usbi_dbg("reusing default context");
 		default_context_refcnt++;
 		usbi_mutex_static_unlock(&default_context_lock);
 		return 0;
 	}
 
-	ctx = calloc(1, PTR_ALIGN(sizeof(*ctx)) + priv_size);
-	if (!ctx) {
-		r = LIBUSB_ERROR_NO_MEM;
-		goto err_unlock;
+	/* check for first init */
+	if (!active_contexts_list.next) {
+		list_init(&active_contexts_list);
+		usbi_get_monotonic_time(&timestamp_origin);
+	}
+
+	_ctx = calloc(1, PTR_ALIGN(sizeof(*_ctx)) + priv_size);
+	if (!_ctx) {
+		usbi_mutex_static_unlock(&default_context_lock);
+		return LIBUSB_ERROR_NO_MEM;
 	}
 
 #if defined(ENABLE_LOGGING) && !defined(ENABLE_DEBUG_LOGGING)
-	ctx->debug = get_env_debug_level();
-	if (ctx->debug != LIBUSB_LOG_LEVEL_NONE)
-		ctx->debug_fixed = 1;
+	_ctx->debug = get_env_debug_level();
+	if (_ctx->debug != LIBUSB_LOG_LEVEL_NONE)
+		_ctx->debug_fixed = 1;
 #endif
 
 	/* default context should be initialized before calling usbi_dbg */
-	if (!usbi_default_context) {
-		usbi_default_context = ctx;
-		default_context_refcnt++;
+	if (!ctx) {
+		usbi_default_context = _ctx;
+		default_context_refcnt = 1;
 		usbi_dbg("created default context");
 	}
 
 	usbi_dbg("libusb v%u.%u.%u.%u%s", libusb_version_internal.major, libusb_version_internal.minor,
 		libusb_version_internal.micro, libusb_version_internal.nano, libusb_version_internal.rc);
 
-	usbi_mutex_init(&ctx->usb_devs_lock);
-	usbi_mutex_init(&ctx->open_devs_lock);
-	usbi_mutex_init(&ctx->hotplug_cbs_lock);
-	list_init(&ctx->usb_devs);
-	list_init(&ctx->open_devs);
-	list_init(&ctx->hotplug_cbs);
-	ctx->next_hotplug_cb_handle = 1;
+	usbi_mutex_init(&_ctx->usb_devs_lock);
+	usbi_mutex_init(&_ctx->open_devs_lock);
+	list_init(&_ctx->usb_devs);
+	list_init(&_ctx->open_devs);
+
+	r = usbi_io_init(_ctx);
+	if (r < 0) {
+		usbi_mutex_static_unlock(&default_context_lock);
+		goto err_free_ctx;
+	}
 
 	usbi_mutex_static_lock(&active_contexts_lock);
-	if (first_init) {
-		first_init = 0;
-		list_init(&active_contexts_list);
-	}
-	list_add (&ctx->list, &active_contexts_list);
+	list_add(&_ctx->list, &active_contexts_list);
 	usbi_mutex_static_unlock(&active_contexts_lock);
 
 	if (usbi_backend.init) {
-		r = usbi_backend.init(ctx);
+		r = usbi_backend.init(_ctx);
 		if (r)
-			goto err_free_ctx;
+			goto err_io_exit;
 	}
 
-	r = usbi_io_init(ctx);
-	if (r < 0)
-		goto err_backend_exit;
+	usbi_hotplug_init(_ctx);
 
 	usbi_mutex_static_unlock(&default_context_lock);
 
-	if (context)
-		*context = ctx;
+	if (ctx)
+		*ctx = _ctx;
 
 	return 0;
 
-err_backend_exit:
-	if (usbi_backend.exit)
-		usbi_backend.exit(ctx);
-err_free_ctx:
-	if (ctx == usbi_default_context) {
-		usbi_default_context = NULL;
-		default_context_refcnt--;
-	}
-
+err_io_exit:
 	usbi_mutex_static_lock(&active_contexts_lock);
-	list_del(&ctx->list);
+	list_del(&_ctx->list);
 	usbi_mutex_static_unlock(&active_contexts_lock);
 
-	usbi_mutex_lock(&ctx->usb_devs_lock);
-	for_each_device_safe(ctx, dev, next) {
-		list_del(&dev->list);
-		libusb_unref_device(dev);
+	if (!ctx) {
+		usbi_default_context = NULL;
+		default_context_refcnt = 0;
 	}
-	usbi_mutex_unlock(&ctx->usb_devs_lock);
 
-	usbi_mutex_destroy(&ctx->open_devs_lock);
-	usbi_mutex_destroy(&ctx->usb_devs_lock);
-	usbi_mutex_destroy(&ctx->hotplug_cbs_lock);
-
-	free(ctx);
-err_unlock:
 	usbi_mutex_static_unlock(&default_context_lock);
+
+	usbi_hotplug_exit(_ctx);
+	usbi_io_exit(_ctx);
+
+err_free_ctx:
+	usbi_mutex_destroy(&_ctx->open_devs_lock);
+	usbi_mutex_destroy(&_ctx->usb_devs_lock);
+
+	free(_ctx);
+
 	return r;
 }
 
@@ -2364,18 +2344,14 @@
  */
 void API_EXPORTED libusb_exit(libusb_context *ctx)
 {
-	struct libusb_device *dev, *next;
-	struct timeval tv = { 0, 0 };
-	int destroying_default_context = 0;
+	struct libusb_context *_ctx;
+	struct libusb_device *dev;
 
-	usbi_dbg(" ");
-
-	ctx = usbi_get_context(ctx);
+	usbi_mutex_static_lock(&default_context_lock);
 
 	/* if working with default context, only actually do the deinitialization
 	 * if we're the last user */
-	usbi_mutex_static_lock(&default_context_lock);
-	if (ctx == usbi_default_context) {
+	if (!ctx) {
 		if (!usbi_default_context) {
 			usbi_dbg("no default context, not initialized?");
 			usbi_mutex_static_unlock(&default_context_lock);
@@ -2387,80 +2363,44 @@
 			usbi_mutex_static_unlock(&default_context_lock);
 			return;
 		}
-		usbi_dbg("destroying default context");
 
-		/*
-		 * Setting this flag without unlocking the default context, as
-		 * we are actually destroying the default context.
-		 * usbi_default_context is not set to NULL yet, as all activities
-		 * would only stop after usbi_backend->exit() returns.
-		 */
-		destroying_default_context = 1;
+		usbi_dbg("destroying default context");
+		_ctx = usbi_default_context;
 	} else {
-		/* Unlock default context, as we're not modifying it. */
-		usbi_mutex_static_unlock(&default_context_lock);
+		usbi_dbg(" ");
+		_ctx = ctx;
 	}
 
 	usbi_mutex_static_lock(&active_contexts_lock);
-	list_del(&ctx->list);
+	list_del(&_ctx->list);
 	usbi_mutex_static_unlock(&active_contexts_lock);
 
-	/* Don't bother with locking after this point because unless there is
-	 * an application bug, nobody will be accessing these. */
-
-	if (libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) {
-		usbi_hotplug_deregister(ctx, 1);
-
-		/*
-		 * Ensure any pending unplug events are read from the hotplug
-		 * pipe. The usb_device-s hold in the events are no longer part
-		 * of usb_devs, but the events still hold a reference!
-		 *
-		 * Note we don't do this if the application has left devices
-		 * open (which implies a buggy app) to avoid packet completion
-		 * handlers running when the app does not expect them to run.
-		 */
-		if (list_empty(&ctx->open_devs))
-			libusb_handle_events_timeout(ctx, &tv);
-
-		for_each_device_safe(ctx, dev, next) {
-			if (usbi_atomic_load(&dev->refcnt) > 1)
-				usbi_warn(ctx, "device %d.%d still referenced",
-					dev->bus_number, dev->device_address);
-			list_del(&dev->list);
-			libusb_unref_device(dev);
-		}
-	} else {
-		/*
-		 * Backends without hotplug store enumerated devices on the
-		 * usb_devs list when libusb_get_device_list() is called.
-		 * These devices are removed from the list when the last
-		 * reference is dropped, typically when the device list is
-		 * freed. Any device still on the list has a reference held
-		 * by the app, which is a bug.
-		 */
-		for_each_device(ctx, dev) {
-			usbi_warn(ctx, "device %d.%d still referenced",
-				dev->bus_number, dev->device_address);
-		}
-	}
-
-	if (!list_empty(&ctx->open_devs))
-		usbi_warn(ctx, "application left some devices open");
-
-	usbi_io_exit(ctx);
 	if (usbi_backend.exit)
-		usbi_backend.exit(ctx);
+		usbi_backend.exit(_ctx);
 
-	usbi_mutex_destroy(&ctx->open_devs_lock);
-	usbi_mutex_destroy(&ctx->usb_devs_lock);
-	usbi_mutex_destroy(&ctx->hotplug_cbs_lock);
-	free(ctx);
-
-	if (destroying_default_context) {
+	if (!ctx)
 		usbi_default_context = NULL;
-		usbi_mutex_static_unlock(&default_context_lock);
+
+	usbi_mutex_static_unlock(&default_context_lock);
+
+	/* Don't bother with locking after this point because unless there is
+	 * an application bug, nobody will be accessing the context. */
+
+	usbi_hotplug_exit(_ctx);
+	usbi_io_exit(_ctx);
+
+	for_each_device(_ctx, dev) {
+		usbi_warn(_ctx, "device %d.%d still referenced",
+			dev->bus_number, dev->device_address);
 	}
+
+	if (!list_empty(&_ctx->open_devs))
+		usbi_warn(_ctx, "application left some devices open");
+
+	usbi_mutex_destroy(&_ctx->open_devs_lock);
+	usbi_mutex_destroy(&_ctx->usb_devs_lock);
+
+	free(_ctx);
 }
 
 /** \ingroup libusb_misc
diff --git a/libusb/hotplug.c b/libusb/hotplug.c
index e3e5e76..387b49f 100644
--- a/libusb/hotplug.c
+++ b/libusb/hotplug.c
@@ -20,7 +20,6 @@
  */
 
 #include "libusbi.h"
-#include "hotplug.h"
 
 /**
  * @defgroup libusb_hotplug Device hotplug event notification
@@ -144,15 +143,68 @@
  */
 
 #define VALID_HOTPLUG_EVENTS			\
-	 (LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED |	\
-	  LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT)
+	(LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED |	\
+	 LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT)
 
 #define VALID_HOTPLUG_FLAGS			\
-	 (LIBUSB_HOTPLUG_ENUMERATE)
+	(LIBUSB_HOTPLUG_ENUMERATE)
 
-static int usbi_hotplug_match_cb(struct libusb_context *ctx,
-	struct libusb_device *dev, libusb_hotplug_event event,
-	struct libusb_hotplug_callback *hotplug_cb)
+void usbi_hotplug_init(struct libusb_context *ctx)
+{
+	/* check for hotplug support */
+	if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG))
+		return;
+
+	usbi_mutex_init(&ctx->hotplug_cbs_lock);
+	list_init(&ctx->hotplug_cbs);
+	ctx->next_hotplug_cb_handle = 1;
+	usbi_atomic_store(&ctx->hotplug_ready, 1);
+}
+
+void usbi_hotplug_exit(struct libusb_context *ctx)
+{
+	struct usbi_hotplug_callback *hotplug_cb, *next_cb;
+	struct usbi_hotplug_message *msg;
+	struct libusb_device *dev, *next_dev;
+
+	/* check for hotplug support */
+	if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG))
+		return;
+
+	/* free all registered hotplug callbacks */
+	for_each_hotplug_cb_safe(ctx, hotplug_cb, next_cb) {
+		list_del(&hotplug_cb->list);
+		free(hotplug_cb);
+	}
+
+	/* free all pending hotplug messages */
+	while (!list_empty(&ctx->hotplug_msgs)) {
+		msg = list_first_entry(&ctx->hotplug_msgs, struct usbi_hotplug_message, list);
+
+		/* if the device left, the message holds a reference
+		 * and we must drop it */
+		if (msg->event == LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT)
+			libusb_unref_device(msg->device);
+
+		list_del(&msg->list);
+		free(msg);
+	}
+
+	/* free all discovered devices */
+	for_each_device_safe(ctx, dev, next_dev) {
+		/* remove the device from the usb_devs list only if there are no
+		 * references held, otherwise leave it on the list so that a
+		 * warning message will be shown */
+		if (usbi_atomic_load(&dev->refcnt) == 1)
+			list_del(&dev->list);
+		libusb_unref_device(dev);
+	}
+
+	usbi_mutex_destroy(&ctx->hotplug_cbs_lock);
+}
+
+static int usbi_hotplug_match_cb(struct libusb_device *dev,
+	libusb_hotplug_event event, struct usbi_hotplug_callback *hotplug_cb)
 {
 	if (!(hotplug_cb->flags & event)) {
 		return 0;
@@ -173,28 +225,82 @@
 		return 0;
 	}
 
-	return hotplug_cb->cb(ctx, dev, event, hotplug_cb->user_data);
+	return hotplug_cb->cb(DEVICE_CTX(dev), dev, event, hotplug_cb->user_data);
 }
 
-void usbi_hotplug_match(struct libusb_context *ctx, struct libusb_device *dev,
+void usbi_hotplug_notification(struct libusb_context *ctx, struct libusb_device *dev,
 	libusb_hotplug_event event)
 {
-	struct libusb_hotplug_callback *hotplug_cb, *next;
-	int ret;
+	struct usbi_hotplug_message *msg;
+	unsigned int event_flags;
+
+	/* Only generate a notification if hotplug is ready. This prevents hotplug
+	 * notifications from being generated during initial enumeration or if the
+	 * backend does not support hotplug. */
+	if (!usbi_atomic_load(&ctx->hotplug_ready))
+		return;
+
+	msg = calloc(1, sizeof(*msg));
+	if (!msg) {
+		usbi_err(ctx, "error allocating hotplug message");
+		return;
+	}
+
+	msg->event = event;
+	msg->device = dev;
+
+	/* Take the event data lock and add this message to the list.
+	 * Only signal an event if there are no prior pending events. */
+	usbi_mutex_lock(&ctx->event_data_lock);
+	event_flags = ctx->event_flags;
+	ctx->event_flags |= USBI_EVENT_HOTPLUG_MSG_PENDING;
+	list_add_tail(&msg->list, &ctx->hotplug_msgs);
+	if (!event_flags)
+		usbi_signal_event(&ctx->event);
+	usbi_mutex_unlock(&ctx->event_data_lock);
+}
+
+void usbi_hotplug_process(struct libusb_context *ctx, struct list_head *hotplug_msgs)
+{
+	struct usbi_hotplug_callback *hotplug_cb, *next_cb;
+	struct usbi_hotplug_message *msg;
+	int r;
 
 	usbi_mutex_lock(&ctx->hotplug_cbs_lock);
 
-	for_each_hotplug_cb_safe(ctx, hotplug_cb, next) {
-		if (hotplug_cb->flags & USBI_HOTPLUG_NEEDS_FREE) {
-			/* process deregistration in usbi_hotplug_deregister() */
-			continue;
+	/* dispatch all pending hotplug messages */
+	while (!list_empty(hotplug_msgs)) {
+		msg = list_first_entry(hotplug_msgs, struct usbi_hotplug_message, list);
+
+		for_each_hotplug_cb_safe(ctx, hotplug_cb, next_cb) {
+			/* skip callbacks that have unregistered */
+			if (hotplug_cb->flags & USBI_HOTPLUG_NEEDS_FREE)
+				continue;
+
+			usbi_mutex_unlock(&ctx->hotplug_cbs_lock);
+			r = usbi_hotplug_match_cb(msg->device, msg->event, hotplug_cb);
+			usbi_mutex_lock(&ctx->hotplug_cbs_lock);
+
+			if (r) {
+				list_del(&hotplug_cb->list);
+				free(hotplug_cb);
+			}
 		}
 
-		usbi_mutex_unlock(&ctx->hotplug_cbs_lock);
-		ret = usbi_hotplug_match_cb(ctx, dev, event, hotplug_cb);
-		usbi_mutex_lock(&ctx->hotplug_cbs_lock);
+		/* if the device left, the message holds a reference
+		 * and we must drop it */
+		if (msg->event == LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT)
+			libusb_unref_device(msg->device);
 
-		if (ret) {
+		list_del(&msg->list);
+		free(msg);
+	}
+
+	/* free any callbacks that have unregistered */
+	for_each_hotplug_cb_safe(ctx, hotplug_cb, next_cb) {
+		if (hotplug_cb->flags & USBI_HOTPLUG_NEEDS_FREE) {
+			usbi_dbg("freeing hotplug cb %p with handle %d",
+				hotplug_cb, hotplug_cb->handle);
 			list_del(&hotplug_cb->list);
 			free(hotplug_cb);
 		}
@@ -203,41 +309,16 @@
 	usbi_mutex_unlock(&ctx->hotplug_cbs_lock);
 }
 
-void usbi_hotplug_notification(struct libusb_context *ctx, struct libusb_device *dev,
-	libusb_hotplug_event event)
-{
-	struct libusb_hotplug_message *message = calloc(1, sizeof(*message));
-	unsigned int event_flags;
-
-	if (!message) {
-		usbi_err(ctx, "error allocating hotplug message");
-		return;
-	}
-
-	message->event = event;
-	message->device = dev;
-
-	/* Take the event data lock and add this message to the list.
-	 * Only signal an event if there are no prior pending events. */
-	usbi_mutex_lock(&ctx->event_data_lock);
-	event_flags = ctx->event_flags;
-	ctx->event_flags |= USBI_EVENT_HOTPLUG_MSG_PENDING;
-	list_add_tail(&message->list, &ctx->hotplug_msgs);
-	if (!event_flags)
-		usbi_signal_event(&ctx->event);
-	usbi_mutex_unlock(&ctx->event_data_lock);
-}
-
 int API_EXPORTED libusb_hotplug_register_callback(libusb_context *ctx,
 	int events, int flags,
 	int vendor_id, int product_id, int dev_class,
 	libusb_hotplug_callback_fn cb_fn, void *user_data,
 	libusb_hotplug_callback_handle *callback_handle)
 {
-	struct libusb_hotplug_callback *new_callback;
+	struct usbi_hotplug_callback *hotplug_cb;
 
 	/* check for sane values */
-	if ((!events || (~VALID_HOTPLUG_EVENTS & events)) ||
+	if (!events || (~VALID_HOTPLUG_EVENTS & events) ||
 	    (~VALID_HOTPLUG_FLAGS & flags) ||
 	    (LIBUSB_HOTPLUG_MATCH_ANY != vendor_id && (~0xffff & vendor_id)) ||
 	    (LIBUSB_HOTPLUG_MATCH_ANY != product_id && (~0xffff & product_id)) ||
@@ -247,47 +328,45 @@
 	}
 
 	/* check for hotplug support */
-	if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) {
+	if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG))
 		return LIBUSB_ERROR_NOT_SUPPORTED;
-	}
 
 	ctx = usbi_get_context(ctx);
 
-	new_callback = calloc(1, sizeof(*new_callback));
-	if (!new_callback) {
+	hotplug_cb = calloc(1, sizeof(*hotplug_cb));
+	if (!hotplug_cb)
 		return LIBUSB_ERROR_NO_MEM;
-	}
 
-	new_callback->flags = (uint8_t)events;
+	hotplug_cb->flags = (uint8_t)events;
 	if (LIBUSB_HOTPLUG_MATCH_ANY != vendor_id) {
-		new_callback->flags |= USBI_HOTPLUG_VENDOR_ID_VALID;
-		new_callback->vendor_id = (uint16_t)vendor_id;
+		hotplug_cb->flags |= USBI_HOTPLUG_VENDOR_ID_VALID;
+		hotplug_cb->vendor_id = (uint16_t)vendor_id;
 	}
 	if (LIBUSB_HOTPLUG_MATCH_ANY != product_id) {
-		new_callback->flags |= USBI_HOTPLUG_PRODUCT_ID_VALID;
-		new_callback->product_id = (uint16_t)product_id;
+		hotplug_cb->flags |= USBI_HOTPLUG_PRODUCT_ID_VALID;
+		hotplug_cb->product_id = (uint16_t)product_id;
 	}
 	if (LIBUSB_HOTPLUG_MATCH_ANY != dev_class) {
-		new_callback->flags |= USBI_HOTPLUG_DEV_CLASS_VALID;
-		new_callback->dev_class = (uint8_t)dev_class;
+		hotplug_cb->flags |= USBI_HOTPLUG_DEV_CLASS_VALID;
+		hotplug_cb->dev_class = (uint8_t)dev_class;
 	}
-	new_callback->cb = cb_fn;
-	new_callback->user_data = user_data;
+	hotplug_cb->cb = cb_fn;
+	hotplug_cb->user_data = user_data;
 
 	usbi_mutex_lock(&ctx->hotplug_cbs_lock);
 
 	/* protect the handle by the context hotplug lock */
-	new_callback->handle = ctx->next_hotplug_cb_handle++;
+	hotplug_cb->handle = ctx->next_hotplug_cb_handle++;
 
 	/* handle the unlikely case of overflow */
 	if (ctx->next_hotplug_cb_handle < 0)
 		ctx->next_hotplug_cb_handle = 1;
 
-	list_add(&new_callback->list, &ctx->hotplug_cbs);
+	list_add(&hotplug_cb->list, &ctx->hotplug_cbs);
 
 	usbi_mutex_unlock(&ctx->hotplug_cbs_lock);
 
-	usbi_dbg("new hotplug cb %p with handle %d", new_callback, new_callback->handle);
+	usbi_dbg("new hotplug cb %p with handle %d", hotplug_cb, hotplug_cb->handle);
 
 	if ((flags & LIBUSB_HOTPLUG_ENUMERATE) && (events & LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED)) {
 		ssize_t i, len;
@@ -295,23 +374,21 @@
 
 		len = libusb_get_device_list(ctx, &devs);
 		if (len < 0) {
-			libusb_hotplug_deregister_callback(ctx,
-							new_callback->handle);
+			libusb_hotplug_deregister_callback(ctx, hotplug_cb->handle);
 			return (int)len;
 		}
 
 		for (i = 0; i < len; i++) {
-			usbi_hotplug_match_cb(ctx, devs[i],
+			usbi_hotplug_match_cb(devs[i],
 					LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED,
-					new_callback);
+					hotplug_cb);
 		}
 
 		libusb_free_device_list(devs, 1);
 	}
 
-
 	if (callback_handle)
-		*callback_handle = new_callback->handle;
+		*callback_handle = hotplug_cb->handle;
 
 	return LIBUSB_SUCCESS;
 }
@@ -319,13 +396,12 @@
 void API_EXPORTED libusb_hotplug_deregister_callback(libusb_context *ctx,
 	libusb_hotplug_callback_handle callback_handle)
 {
-	struct libusb_hotplug_callback *hotplug_cb;
+	struct usbi_hotplug_callback *hotplug_cb;
 	int deregistered = 0;
 
 	/* check for hotplug support */
-	if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) {
+	if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG))
 		return;
-	}
 
 	usbi_dbg("deregister hotplug cb %d", callback_handle);
 
@@ -334,9 +410,10 @@
 	usbi_mutex_lock(&ctx->hotplug_cbs_lock);
 	for_each_hotplug_cb(ctx, hotplug_cb) {
 		if (callback_handle == hotplug_cb->handle) {
-			/* Mark this callback for deregistration */
+			/* mark this callback for deregistration */
 			hotplug_cb->flags |= USBI_HOTPLUG_NEEDS_FREE;
 			deregistered = 1;
+			break;
 		}
 	}
 	usbi_mutex_unlock(&ctx->hotplug_cbs_lock);
@@ -357,15 +434,14 @@
 void * LIBUSB_CALL libusb_hotplug_get_user_data(libusb_context *ctx,
 	libusb_hotplug_callback_handle callback_handle)
 {
-	struct libusb_hotplug_callback *hotplug_cb;
+	struct usbi_hotplug_callback *hotplug_cb;
 	void *user_data = NULL;
 
 	/* check for hotplug support */
-	if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) {
+	if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG))
 		return NULL;
-	}
 
-	usbi_dbg("get hotplug user data %d", callback_handle);
+	usbi_dbg("get hotplug cb %d user data", callback_handle);
 
 	ctx = usbi_get_context(ctx);
 
@@ -373,25 +449,10 @@
 	for_each_hotplug_cb(ctx, hotplug_cb) {
 		if (callback_handle == hotplug_cb->handle) {
 			user_data = hotplug_cb->user_data;
+			break;
 		}
 	}
 	usbi_mutex_unlock(&ctx->hotplug_cbs_lock);
 
 	return user_data;
 }
-
-void usbi_hotplug_deregister(struct libusb_context *ctx, int forced)
-{
-	struct libusb_hotplug_callback *hotplug_cb, *next;
-
-	usbi_mutex_lock(&ctx->hotplug_cbs_lock);
-	for_each_hotplug_cb_safe(ctx, hotplug_cb, next) {
-		if (forced || (hotplug_cb->flags & USBI_HOTPLUG_NEEDS_FREE)) {
-			usbi_dbg("freeing hotplug cb %p with handle %d", hotplug_cb,
-				 hotplug_cb->handle);
-			list_del(&hotplug_cb->list);
-			free(hotplug_cb);
-		}
-	}
-	usbi_mutex_unlock(&ctx->hotplug_cbs_lock);
-}
diff --git a/libusb/hotplug.h b/libusb/hotplug.h
deleted file mode 100644
index 161f7e5..0000000
--- a/libusb/hotplug.h
+++ /dev/null
@@ -1,105 +0,0 @@
-/* -*- Mode: C; indent-tabs-mode:t ; c-basic-offset:8 -*- */
-/*
- * Hotplug support for libusb
- * Copyright © 2012-2013 Nathan Hjelm <hjelmn@mac.com>
- * Copyright © 2012-2013 Peter Stuge <peter@stuge.se>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
-
-#ifndef USBI_HOTPLUG_H
-#define USBI_HOTPLUG_H
-
-#include "libusbi.h"
-
-enum usbi_hotplug_flags {
-	/* This callback is interested in device arrivals */
-	USBI_HOTPLUG_DEVICE_ARRIVED = LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED,
-
-	/* This callback is interested in device removals */
-	USBI_HOTPLUG_DEVICE_LEFT = LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT,
-
-	/* IMPORTANT: The values for the below entries must start *after*
-	 * the highest value of the above entries!!!
-	 */
-
-	/* The vendor_id field is valid for matching */
-	USBI_HOTPLUG_VENDOR_ID_VALID = (1U << 3),
-
-	/* The product_id field is valid for matching */
-	USBI_HOTPLUG_PRODUCT_ID_VALID = (1U << 4),
-
-	/* The dev_class field is valid for matching */
-	USBI_HOTPLUG_DEV_CLASS_VALID = (1U << 5),
-
-	/* This callback has been unregistered and needs to be freed */
-	USBI_HOTPLUG_NEEDS_FREE = (1U << 6),
-};
-
-/** \ingroup hotplug
- * The hotplug callback structure. The user populates this structure with
- * libusb_hotplug_prepare_callback() and then calls libusb_hotplug_register_callback()
- * to receive notification of hotplug events.
- */
-struct libusb_hotplug_callback {
-	/** Flags that control how this callback behaves */
-	uint8_t flags;
-
-	/** Vendor ID to match (if flags says this is valid) */
-	uint16_t vendor_id;
-
-	/** Product ID to match (if flags says this is valid) */
-	uint16_t product_id;
-
-	/** Device class to match (if flags says this is valid) */
-	uint8_t dev_class;
-
-	/** Callback function to invoke for matching event/device */
-	libusb_hotplug_callback_fn cb;
-
-	/** Handle for this callback (used to match on deregister) */
-	libusb_hotplug_callback_handle handle;
-
-	/** User data that will be passed to the callback function */
-	void *user_data;
-
-	/** List this callback is registered in (ctx->hotplug_cbs) */
-	struct list_head list;
-};
-
-struct libusb_hotplug_message {
-	/** The hotplug event that occurred */
-	libusb_hotplug_event event;
-
-	/** The device for which this hotplug event occurred */
-	struct libusb_device *device;
-
-	/** List this message is contained in (ctx->hotplug_msgs) */
-	struct list_head list;
-};
-
-#define for_each_hotplug_cb(ctx, c) \
-	for_each_helper(c, &(ctx)->hotplug_cbs, struct libusb_hotplug_callback)
-
-#define for_each_hotplug_cb_safe(ctx, c, n) \
-	for_each_safe_helper(c, n, &(ctx)->hotplug_cbs, struct libusb_hotplug_callback)
-
-void usbi_hotplug_deregister(struct libusb_context *ctx, int forced);
-void usbi_hotplug_match(struct libusb_context *ctx, struct libusb_device *dev,
-	libusb_hotplug_event event);
-void usbi_hotplug_notification(struct libusb_context *ctx, struct libusb_device *dev,
-	libusb_hotplug_event event);
-
-#endif
diff --git a/libusb/io.c b/libusb/io.c
index 0e960dd..ecd750f 100644
--- a/libusb/io.c
+++ b/libusb/io.c
@@ -22,7 +22,6 @@
  */
 
 #include "libusbi.h"
-#include "hotplug.h"
 
 /**
  * \page libusb_io Synchronous and asynchronous device I/O
@@ -2073,6 +2072,7 @@
 static int handle_event_trigger(struct libusb_context *ctx)
 {
 	struct list_head hotplug_msgs;
+	int hotplug_event = 0;
 	int r = 0;
 
 	usbi_dbg("event triggered");
@@ -2091,6 +2091,12 @@
 		ctx->event_flags &= ~USBI_EVENT_USER_INTERRUPT;
 	}
 
+	if (ctx->event_flags & USBI_EVENT_HOTPLUG_CB_DEREGISTERED) {
+		usbi_dbg("someone unregistered a hotplug cb");
+		ctx->event_flags &= ~USBI_EVENT_HOTPLUG_CB_DEREGISTERED;
+		hotplug_event = 1;
+	}
+
 	/* check if someone is closing a device */
 	if (ctx->event_flags & USBI_EVENT_DEVICE_CLOSE)
 		usbi_dbg("someone is closing a device");
@@ -2099,6 +2105,7 @@
 	if (ctx->event_flags & USBI_EVENT_HOTPLUG_MSG_PENDING) {
 		usbi_dbg("hotplug message received");
 		ctx->event_flags &= ~USBI_EVENT_HOTPLUG_MSG_PENDING;
+		hotplug_event = 1;
 		assert(!list_empty(&ctx->hotplug_msgs));
 		list_cut(&hotplug_msgs, &ctx->hotplug_msgs);
 	}
@@ -2136,20 +2143,9 @@
 
 	usbi_mutex_unlock(&ctx->event_data_lock);
 
-	/* process the hotplug messages, if any */
-	while (!list_empty(&hotplug_msgs)) {
-		struct libusb_hotplug_message *message =
-			list_first_entry(&hotplug_msgs, struct libusb_hotplug_message, list);
-
-		usbi_hotplug_match(ctx, message->device, message->event);
-
-		/* the device left, dereference the device */
-		if (message->event == LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT)
-			libusb_unref_device(message->device);
-
-		list_del(&message->list);
-		free(message);
-	}
+	/* process the hotplug events, if any */
+	if (hotplug_event)
+		usbi_hotplug_process(ctx, &hotplug_msgs);
 
 	return r;
 }
diff --git a/libusb/libusbi.h b/libusb/libusbi.h
index 7656660..97c894e 100644
--- a/libusb/libusbi.h
+++ b/libusb/libusbi.h
@@ -368,6 +368,9 @@
 	libusb_hotplug_callback_handle next_hotplug_cb_handle;
 	usbi_mutex_t hotplug_cbs_lock;
 
+	/* A flag to indicate that the context is ready for hotplug notifications */
+	usbi_atomic_t hotplug_ready;
+
 	/* this is a list of in-flight transfer handles, sorted by timeout
 	 * expiration. URBs to timeout the soonest are placed at the beginning of
 	 * the list, URBs that will time out later are placed after, and urbs with
@@ -689,8 +692,75 @@
         uint16_t align;         /* Force 2-byte alignment */
 };
 
+enum usbi_hotplug_flags {
+	/* This callback is interested in device arrivals */
+	USBI_HOTPLUG_DEVICE_ARRIVED = LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED,
+
+	/* This callback is interested in device removals */
+	USBI_HOTPLUG_DEVICE_LEFT = LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT,
+
+	/* IMPORTANT: The values for the below entries must start *after*
+	 * the highest value of the above entries!!!
+	 */
+
+	/* The vendor_id field is valid for matching */
+	USBI_HOTPLUG_VENDOR_ID_VALID = (1U << 3),
+
+	/* The product_id field is valid for matching */
+	USBI_HOTPLUG_PRODUCT_ID_VALID = (1U << 4),
+
+	/* The dev_class field is valid for matching */
+	USBI_HOTPLUG_DEV_CLASS_VALID = (1U << 5),
+
+	/* This callback has been unregistered and needs to be freed */
+	USBI_HOTPLUG_NEEDS_FREE = (1U << 6),
+};
+
+struct usbi_hotplug_callback {
+	/* Flags that control how this callback behaves */
+	uint8_t flags;
+
+	/* Vendor ID to match (if flags says this is valid) */
+	uint16_t vendor_id;
+
+	/* Product ID to match (if flags says this is valid) */
+	uint16_t product_id;
+
+	/* Device class to match (if flags says this is valid) */
+	uint8_t dev_class;
+
+	/* Callback function to invoke for matching event/device */
+	libusb_hotplug_callback_fn cb;
+
+	/* Handle for this callback (used to match on deregister) */
+	libusb_hotplug_callback_handle handle;
+
+	/* User data that will be passed to the callback function */
+	void *user_data;
+
+	/* List this callback is registered in (ctx->hotplug_cbs) */
+	struct list_head list;
+};
+
+struct usbi_hotplug_message {
+	/* The hotplug event that occurred */
+	libusb_hotplug_event event;
+
+	/* The device for which this hotplug event occurred */
+	struct libusb_device *device;
+
+	/* List this message is contained in (ctx->hotplug_msgs) */
+	struct list_head list;
+};
+
 /* shared data and functions */
 
+void usbi_hotplug_init(struct libusb_context *ctx);
+void usbi_hotplug_exit(struct libusb_context *ctx);
+void usbi_hotplug_notification(struct libusb_context *ctx, struct libusb_device *dev,
+	libusb_hotplug_event event);
+void usbi_hotplug_process(struct libusb_context *ctx, struct list_head *hotplug_msgs);
+
 int usbi_io_init(struct libusb_context *ctx);
 void usbi_io_exit(struct libusb_context *ctx);
 
@@ -818,7 +888,8 @@
 	 * data structures for later, etc.
 	 *
 	 * This function is called when a libusb user initializes the library
-	 * prior to use.
+	 * prior to use. Mutual exclusion with other init and exit calls is
+	 * guaranteed when this function is called.
 	 *
 	 * Return 0 on success, or a LIBUSB_ERROR code on failure.
 	 */
@@ -828,6 +899,8 @@
 	 * that was set up by init.
 	 *
 	 * This function is called when the user deinitializes the library.
+	 * Mutual exclusion with other init and exit calls is guaranteed when
+	 * this function is called.
 	 */
 	void (*exit)(struct libusb_context *ctx);
 
@@ -1390,6 +1463,12 @@
 #define for_each_removed_event_source_safe(ctx, e, n) \
 	for_each_safe_helper(e, n, &(ctx)->removed_event_sources, struct usbi_event_source)
 
+#define for_each_hotplug_cb(ctx, c) \
+	for_each_helper(c, &(ctx)->hotplug_cbs, struct usbi_hotplug_callback)
+
+#define for_each_hotplug_cb_safe(ctx, c, n) \
+	for_each_safe_helper(c, n, &(ctx)->hotplug_cbs, struct usbi_hotplug_callback)
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/libusb/os/darwin_usb.c b/libusb/os/darwin_usb.c
index 2e64038..acfdf97 100644
--- a/libusb/os/darwin_usb.c
+++ b/libusb/os/darwin_usb.c
@@ -52,7 +52,6 @@
 
 #include "darwin_usb.h"
 
-static pthread_mutex_t libusb_darwin_init_mutex = PTHREAD_MUTEX_INITIALIZER;
 static int init_count = 0;
 
 /* async event thread */
@@ -581,8 +580,6 @@
   bool first_init;
   int rc;
 
-  pthread_mutex_lock (&libusb_darwin_init_mutex);
-
   first_init = (1 == ++init_count);
 
   do {
@@ -638,16 +635,12 @@
     --init_count;
   }
 
-  pthread_mutex_unlock (&libusb_darwin_init_mutex);
-
   return rc;
 }
 
 static void darwin_exit (struct libusb_context *ctx) {
   UNUSED(ctx);
 
-  pthread_mutex_lock (&libusb_darwin_init_mutex);
-
   if (0 == --init_count) {
     /* stop the event runloop and wait for the thread to terminate. */
     pthread_mutex_lock (&libusb_darwin_at_mutex);
@@ -665,8 +658,6 @@
     mach_port_deallocate(mach_task_self(), clock_monotonic);
 #endif
   }
-
-  pthread_mutex_unlock (&libusb_darwin_init_mutex);
 }
 
 static int get_configuration_index (struct libusb_device *dev, UInt8 config_value) {
diff --git a/libusb/os/linux_usbfs.c b/libusb/os/linux_usbfs.c
index 3a1894c..5b57993 100644
--- a/libusb/os/linux_usbfs.c
+++ b/libusb/os/linux_usbfs.c
@@ -100,8 +100,6 @@
 static int weak_authority = 0;
 #endif
 
-/* Serialize hotplug start/stop */
-static usbi_mutex_static_t linux_hotplug_startstop_lock = USBI_MUTEX_INITIALIZER;
 /* Serialize scan-devices, event-thread, and poll */
 usbi_mutex_static_t linux_hotplug_lock = USBI_MUTEX_INITIALIZER;
 
@@ -407,7 +405,6 @@
 	}
 #endif
 
-	usbi_mutex_static_lock(&linux_hotplug_startstop_lock);
 	r = LIBUSB_SUCCESS;
 	if (init_count == 0) {
 		/* start up hotplug event handler */
@@ -422,7 +419,6 @@
 	} else {
 		usbi_err(ctx, "error starting hotplug event monitor");
 	}
-	usbi_mutex_static_unlock(&linux_hotplug_startstop_lock);
 
 	return r;
 }
@@ -437,13 +433,11 @@
 	}
 #endif
 
-	usbi_mutex_static_lock(&linux_hotplug_startstop_lock);
 	assert(init_count != 0);
 	if (!--init_count) {
 		/* tear down event handler */
 		linux_stop_event_monitor();
 	}
-	usbi_mutex_static_unlock(&linux_hotplug_startstop_lock);
 }
 
 static int op_set_option(struct libusb_context *ctx, enum libusb_option option, va_list ap)
diff --git a/libusb/os/windows_common.c b/libusb/os/windows_common.c
index 6e69317..72499ee 100644
--- a/libusb/os/windows_common.c
+++ b/libusb/os/windows_common.c
@@ -477,26 +477,9 @@
 static int windows_init(struct libusb_context *ctx)
 {
 	struct windows_context_priv *priv = usbi_get_context_priv(ctx);
-	char mutex_name[11 + 8 + 1]; // strlen("libusb_init") + (32-bit hex PID) + '\0'
-	HANDLE mutex;
 	bool winusb_backend_init = false;
 	int r;
 
-	sprintf(mutex_name, "libusb_init%08lX", ULONG_CAST(GetCurrentProcessId() & 0xFFFFFFFFU));
-	mutex = CreateMutexA(NULL, FALSE, mutex_name);
-	if (mutex == NULL) {
-		usbi_err(ctx, "could not create mutex: %s", windows_error_str(0));
-		return LIBUSB_ERROR_NO_MEM;
-	}
-
-	// A successful wait gives this thread ownership of the mutex
-	// => any concurrent wait stalls until the mutex is released
-	if (WaitForSingleObject(mutex, INFINITE) != WAIT_OBJECT_0) {
-		usbi_err(ctx, "failure to access mutex: %s", windows_error_str(0));
-		CloseHandle(mutex);
-		return LIBUSB_ERROR_NO_MEM;
-	}
-
 	// NB: concurrent usage supposes that init calls are equally balanced with
 	// exit calls. If init is called more than exit, we will not exit properly
 	if (++init_count == 1) { // First init?
@@ -565,29 +548,12 @@
 		--init_count;
 	}
 
-	ReleaseMutex(mutex);
-	CloseHandle(mutex);
 	return r;
 }
 
 static void windows_exit(struct libusb_context *ctx)
 {
 	struct windows_context_priv *priv = usbi_get_context_priv(ctx);
-	char mutex_name[11 + 8 + 1]; // strlen("libusb_init") + (32-bit hex PID) + '\0'
-	HANDLE mutex;
-
-	sprintf(mutex_name, "libusb_init%08lX", ULONG_CAST(GetCurrentProcessId() & 0xFFFFFFFFU));
-	mutex = CreateMutexA(NULL, FALSE, mutex_name);
-	if (mutex == NULL)
-		return;
-
-	// A successful wait gives this thread ownership of the mutex
-	// => any concurrent wait stalls until the mutex is released
-	if (WaitForSingleObject(mutex, INFINITE) != WAIT_OBJECT_0) {
-		usbi_err(ctx, "failed to access mutex: %s", windows_error_str(0));
-		CloseHandle(mutex);
-		return;
-	}
 
 	// A NULL completion status will indicate to the thread that it is time to exit
 	if (!PostQueuedCompletionStatus(priv->completion_port, 0, (ULONG_PTR)ctx, NULL))
@@ -608,9 +574,6 @@
 		winusb_backend.exit(ctx);
 		htab_destroy();
 	}
-
-	ReleaseMutex(mutex);
-	CloseHandle(mutex);
 }
 
 static int windows_set_option(struct libusb_context *ctx, enum libusb_option option, va_list ap)
diff --git a/msvc/libusb_dll_2013.vcxproj b/msvc/libusb_dll_2013.vcxproj
index 56ffd75..03212dc 100644
--- a/msvc/libusb_dll_2013.vcxproj
+++ b/msvc/libusb_dll_2013.vcxproj
@@ -83,7 +83,6 @@
   <ItemGroup>
     <ClInclude Include=".\config.h" />
     <ClInclude Include="..\libusb\os\events_windows.h" />
-    <ClInclude Include="..\libusb\hotplug.h" />
     <ClInclude Include="..\libusb\libusb.h" />
     <ClInclude Include="..\libusb\libusbi.h" />
     <ClInclude Include="..\libusb\os\threads_windows.h" />
diff --git a/msvc/libusb_dll_2013.vcxproj.filters b/msvc/libusb_dll_2013.vcxproj.filters
index 8da28e3..c8643f2 100644
--- a/msvc/libusb_dll_2013.vcxproj.filters
+++ b/msvc/libusb_dll_2013.vcxproj.filters
@@ -21,9 +21,6 @@
     <ClInclude Include="..\libusb\os\events_windows.h">
       <Filter>Header Files</Filter>
     </ClInclude>
-    <ClInclude Include="..\libusb\hotplug.h">
-      <Filter>Header Files</Filter>
-    </ClInclude>
     <ClInclude Include="..\libusb\libusb.h">
       <Filter>Header Files</Filter>
     </ClInclude>
diff --git a/msvc/libusb_dll_2015.vcxproj b/msvc/libusb_dll_2015.vcxproj
index d2c850d..f24d94b 100644
--- a/msvc/libusb_dll_2015.vcxproj
+++ b/msvc/libusb_dll_2015.vcxproj
@@ -84,7 +84,6 @@
   <ItemGroup>
     <ClInclude Include=".\config.h" />
     <ClInclude Include="..\libusb\os\events_windows.h" />
-    <ClInclude Include="..\libusb\hotplug.h" />
     <ClInclude Include="..\libusb\libusb.h" />
     <ClInclude Include="..\libusb\libusbi.h" />
     <ClInclude Include="..\libusb\os\threads_windows.h" />
diff --git a/msvc/libusb_dll_2015.vcxproj.filters b/msvc/libusb_dll_2015.vcxproj.filters
index 8da28e3..c8643f2 100644
--- a/msvc/libusb_dll_2015.vcxproj.filters
+++ b/msvc/libusb_dll_2015.vcxproj.filters
@@ -21,9 +21,6 @@
     <ClInclude Include="..\libusb\os\events_windows.h">
       <Filter>Header Files</Filter>
     </ClInclude>
-    <ClInclude Include="..\libusb\hotplug.h">
-      <Filter>Header Files</Filter>
-    </ClInclude>
     <ClInclude Include="..\libusb\libusb.h">
       <Filter>Header Files</Filter>
     </ClInclude>
diff --git a/msvc/libusb_dll_2017.vcxproj b/msvc/libusb_dll_2017.vcxproj
index 598159d..2ff2f94 100644
--- a/msvc/libusb_dll_2017.vcxproj
+++ b/msvc/libusb_dll_2017.vcxproj
@@ -103,7 +103,6 @@
   <ItemGroup>
     <ClInclude Include=".\config.h" />
     <ClInclude Include="..\libusb\os\events_windows.h" />
-    <ClInclude Include="..\libusb\hotplug.h" />
     <ClInclude Include="..\libusb\libusb.h" />
     <ClInclude Include="..\libusb\libusbi.h" />
     <ClInclude Include="..\libusb\os\threads_windows.h" />
diff --git a/msvc/libusb_dll_2017.vcxproj.filters b/msvc/libusb_dll_2017.vcxproj.filters
index 8da28e3..c8643f2 100644
--- a/msvc/libusb_dll_2017.vcxproj.filters
+++ b/msvc/libusb_dll_2017.vcxproj.filters
@@ -21,9 +21,6 @@
     <ClInclude Include="..\libusb\os\events_windows.h">
       <Filter>Header Files</Filter>
     </ClInclude>
-    <ClInclude Include="..\libusb\hotplug.h">
-      <Filter>Header Files</Filter>
-    </ClInclude>
     <ClInclude Include="..\libusb\libusb.h">
       <Filter>Header Files</Filter>
     </ClInclude>
diff --git a/msvc/libusb_dll_2019.vcxproj b/msvc/libusb_dll_2019.vcxproj
index dbd8717..266166e 100644
--- a/msvc/libusb_dll_2019.vcxproj
+++ b/msvc/libusb_dll_2019.vcxproj
@@ -103,7 +103,6 @@
   <ItemGroup>
     <ClInclude Include=".\config.h" />
     <ClInclude Include="..\libusb\os\events_windows.h" />
-    <ClInclude Include="..\libusb\hotplug.h" />
     <ClInclude Include="..\libusb\libusb.h" />
     <ClInclude Include="..\libusb\libusbi.h" />
     <ClInclude Include="..\libusb\os\threads_windows.h" />
diff --git a/msvc/libusb_dll_2019.vcxproj.filters b/msvc/libusb_dll_2019.vcxproj.filters
index 8da28e3..c8643f2 100644
--- a/msvc/libusb_dll_2019.vcxproj.filters
+++ b/msvc/libusb_dll_2019.vcxproj.filters
@@ -21,9 +21,6 @@
     <ClInclude Include="..\libusb\os\events_windows.h">
       <Filter>Header Files</Filter>
     </ClInclude>
-    <ClInclude Include="..\libusb\hotplug.h">
-      <Filter>Header Files</Filter>
-    </ClInclude>
     <ClInclude Include="..\libusb\libusb.h">
       <Filter>Header Files</Filter>
     </ClInclude>
diff --git a/msvc/libusb_static_2013.vcxproj b/msvc/libusb_static_2013.vcxproj
index 1b287e5..94ba597 100644
--- a/msvc/libusb_static_2013.vcxproj
+++ b/msvc/libusb_static_2013.vcxproj
@@ -79,7 +79,6 @@
   <ItemGroup>
     <ClInclude Include=".\config.h" />
     <ClInclude Include="..\libusb\os\events_windows.h" />
-    <ClInclude Include="..\libusb\hotplug.h" />
     <ClInclude Include="..\libusb\libusb.h" />
     <ClInclude Include="..\libusb\libusbi.h" />
     <ClInclude Include="..\libusb\os\threads_windows.h" />
diff --git a/msvc/libusb_static_2013.vcxproj.filters b/msvc/libusb_static_2013.vcxproj.filters
index 2994ca1..a3294da 100644
--- a/msvc/libusb_static_2013.vcxproj.filters
+++ b/msvc/libusb_static_2013.vcxproj.filters
@@ -17,9 +17,6 @@
     <ClInclude Include="..\libusb\os\events_windows.h">
       <Filter>Header Files</Filter>
     </ClInclude>
-    <ClInclude Include="..\libusb\hotplug.h">
-      <Filter>Header Files</Filter>
-    </ClInclude>
     <ClInclude Include="..\libusb\libusb.h">
       <Filter>Header Files</Filter>
     </ClInclude>
diff --git a/msvc/libusb_static_2015.vcxproj b/msvc/libusb_static_2015.vcxproj
index 9fa30da..f951523 100644
--- a/msvc/libusb_static_2015.vcxproj
+++ b/msvc/libusb_static_2015.vcxproj
@@ -80,7 +80,6 @@
   <ItemGroup>
     <ClInclude Include=".\config.h" />
     <ClInclude Include="..\libusb\os\events_windows.h" />
-    <ClInclude Include="..\libusb\hotplug.h" />
     <ClInclude Include="..\libusb\libusb.h" />
     <ClInclude Include="..\libusb\libusbi.h" />
     <ClInclude Include="..\libusb\os\threads_windows.h" />
diff --git a/msvc/libusb_static_2015.vcxproj.filters b/msvc/libusb_static_2015.vcxproj.filters
index 2994ca1..a3294da 100644
--- a/msvc/libusb_static_2015.vcxproj.filters
+++ b/msvc/libusb_static_2015.vcxproj.filters
@@ -17,9 +17,6 @@
     <ClInclude Include="..\libusb\os\events_windows.h">
       <Filter>Header Files</Filter>
     </ClInclude>
-    <ClInclude Include="..\libusb\hotplug.h">
-      <Filter>Header Files</Filter>
-    </ClInclude>
     <ClInclude Include="..\libusb\libusb.h">
       <Filter>Header Files</Filter>
     </ClInclude>
diff --git a/msvc/libusb_static_2017.vcxproj b/msvc/libusb_static_2017.vcxproj
index 62076e0..857ee3f 100644
--- a/msvc/libusb_static_2017.vcxproj
+++ b/msvc/libusb_static_2017.vcxproj
@@ -99,7 +99,6 @@
   <ItemGroup>
     <ClInclude Include=".\config.h" />
     <ClInclude Include="..\libusb\os\events_windows.h" />
-    <ClInclude Include="..\libusb\hotplug.h" />
     <ClInclude Include="..\libusb\libusb.h" />
     <ClInclude Include="..\libusb\libusbi.h" />
     <ClInclude Include="..\libusb\os\threads_windows.h" />
diff --git a/msvc/libusb_static_2017.vcxproj.filters b/msvc/libusb_static_2017.vcxproj.filters
index 2994ca1..a3294da 100644
--- a/msvc/libusb_static_2017.vcxproj.filters
+++ b/msvc/libusb_static_2017.vcxproj.filters
@@ -17,9 +17,6 @@
     <ClInclude Include="..\libusb\os\events_windows.h">
       <Filter>Header Files</Filter>
     </ClInclude>
-    <ClInclude Include="..\libusb\hotplug.h">
-      <Filter>Header Files</Filter>
-    </ClInclude>
     <ClInclude Include="..\libusb\libusb.h">
       <Filter>Header Files</Filter>
     </ClInclude>
diff --git a/msvc/libusb_static_2019.vcxproj b/msvc/libusb_static_2019.vcxproj
index 60ad642..036ce95 100644
--- a/msvc/libusb_static_2019.vcxproj
+++ b/msvc/libusb_static_2019.vcxproj
@@ -99,7 +99,6 @@
   <ItemGroup>
     <ClInclude Include=".\config.h" />
     <ClInclude Include="..\libusb\os\events_windows.h" />
-    <ClInclude Include="..\libusb\hotplug.h" />
     <ClInclude Include="..\libusb\libusb.h" />
     <ClInclude Include="..\libusb\libusbi.h" />
     <ClInclude Include="..\libusb\os\threads_windows.h" />
diff --git a/msvc/libusb_static_2019.vcxproj.filters b/msvc/libusb_static_2019.vcxproj.filters
index 2994ca1..a3294da 100644
--- a/msvc/libusb_static_2019.vcxproj.filters
+++ b/msvc/libusb_static_2019.vcxproj.filters
@@ -17,9 +17,6 @@
     <ClInclude Include="..\libusb\os\events_windows.h">
       <Filter>Header Files</Filter>
     </ClInclude>
-    <ClInclude Include="..\libusb\hotplug.h">
-      <Filter>Header Files</Filter>
-    </ClInclude>
     <ClInclude Include="..\libusb\libusb.h">
       <Filter>Header Files</Filter>
     </ClInclude>