BLE Event Handler Dispatch Devirtualized
Virtual handler interfaces (GAPEventHandler, GAPScanEventHandler, GATTcEventHandler, GATTsEventHandler, BLEStatusEventHandler) have been replaced with callback-based dispatch. External components that inherit from these interfaces must update their registration approach.
This is a developer breaking change for external components in ESPHome 2026.4.0 and later.
Background
Section titled “Background”PR #15310: Devirtualize BLE event handler dispatch
The previous virtual handler pattern required vtable lookups and multiple-inheritance this-pointer adjustment thunks on every BLE event dispatch. The new approach uses StaticCallbackManager with lambda callbacks, eliminating vtable overhead. Each lambda captures a single pointer (fits in Callback inline storage, no heap allocation).
Before (virtual dispatch):
ESP32BLE::loop() → vtable load → indirect call → thunk this-adjust → gap_scan_event_handler()After (callback dispatch):
ESP32BLE::loop() → StaticCallbackManager::call() → Callback::fn_(ctx_, args) → direct callWhat’s Changing
Section titled “What’s Changing”The following virtual handler base classes are removed:
GAPEventHandlerGAPScanEventHandlerGATTcEventHandlerGATTsEventHandlerBLEStatusEventHandler
Registration now uses add_*_callback() methods with lambdas instead of register_*_handler() with object pointers. The Python register_* helper functions are unchanged — they generate lambda-based callbacks internally.
Who This Affects
Section titled “Who This Affects”External ESP32 BLE components that inherit from the virtual handler classes.
Migration Guide
Section titled “Migration Guide”C++ changes
Section titled “C++ changes”- Remove the handler base class from your inheritance list
- Remove
overridefrom handler methods
// Beforeclass MyComponent : public Component, public GAPScanEventHandler { void gap_scan_event_handler(const BLEScanResult &scan_result) override;};
// Afterclass MyComponent : public Component { void gap_scan_event_handler(const BLEScanResult &scan_result);};Python codegen
Section titled “Python codegen”The Python register_* helper functions are unchanged — no Python changes needed:
# This still works — generates lambda-based callbacks internallyesp32_ble.register_gap_scan_event_handler(parent, var)Supporting Multiple ESPHome Versions
Section titled “Supporting Multiple ESPHome Versions”// Before 2026.4.0, inherit from the handler class// After 2026.4.0, just declare the method
#if ESPHOME_VERSION_CODE >= VERSION_CODE(2026, 4, 0)class MyComponent : public Component {#elseclass MyComponent : public Component, public GAPScanEventHandler {#endif void gap_scan_event_handler(const BLEScanResult &scan_result)#if ESPHOME_VERSION_CODE < VERSION_CODE(2026, 4, 0) override#endif ;};Timeline
Section titled “Timeline”- ESPHome 2026.4.0 (April 2026): Virtual handler classes removed
- No deprecation period — this is a class removal
Finding Code That Needs Updates
Section titled “Finding Code That Needs Updates”# Find handler class inheritancegrep -rn 'GAPEventHandler\|GAPScanEventHandler\|GATTcEventHandler\|GATTsEventHandler\|BLEStatusEventHandler' your_component/Questions?
Section titled “Questions?”If you have questions about migrating your external component, please ask in:
- ESPHome Discord - #devs channel
- ESPHome GitHub Discussions
Related Documentation
Section titled “Related Documentation”Copyright © 2026 ESPHome - A project from the Open Home Foundation
Comments
Feel free to leave a comment here to discuss this post with others. You can ask questions, share your experience, or suggest improvements. If you have a question about a specific feature or issue, please consider using the ESPHome Discord. Stick to English and follow ESPHome's code of conduct. These comments exist on a discussion on GitHub, so you can also comment there directly if you prefer.