| # Gabeldorsche Architecture |
| |
| [TOC] |
| |
| This document outlines some architectural considerations we've made when |
| developing the Gabeldorsche (GD) Bluetooth stack. |
| |
| ## Threading model |
| |
| First of all, the GD stack does not build on concepts of threads. Instead, it |
| works with [`Handlers`](#handler). However, since GD ultimately runs on an OS, |
| it still needs to interact with processes and threads before achieving the |
| [`Handler`](#handler) abstraction. |
| |
| ### Processes |
| |
| In general. three types of processes exist in the GD runtime environment: |
| |
| **Application processes** |
| : include third-party apps, other system components such as audio and telecom |
| services that interact with the _Bluetooth stack process_ APIs defined |
| through various RPC/IPC methods such as Binder, Socket IPC, gRPC, DBUS. and |
| so on, using languages such as AIDL or Protobuf. For Android applications, |
| although APIs are defined in AIDL, some boiler plate code is wrapped in Java |
| libraries exposed through code in |
| [`frameworks/base/core/java/android/bluetooth`](https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/bluetooth/) |
| that is released to developers as |
| [Android SDK](https://developer.android.com/guide/topics/connectivity/bluetooth). |
| |
| **Hardware abstraction layer (HAL) processes** |
| : one or many processes from the vendor partition, and hence is hardware |
| depenedent. They interact with the _Bluetooth stack process_ via a set of |
| hardware abstraction APIs defined through RPC/IPC methods such as Binder, |
| Socket IPC, DBUS, and so on, using languages such as HIDL. On Android, this |
| would be HAL processes that implement HIDL APIs such as |
| [IBluetoothHci](https://android.googlesource.com/platform/hardware/interfaces/+/refs/heads/master/bluetooth/1.1/IBluetoothHci.hal) |
| and |
| [IBluetoothAudioProvider](https://android.googlesource.com/platform/hardware/interfaces/+/refs/heads/master/bluetooth/audio/2.0/IBluetoothAudioProvider.hal). |
| |
| **Bluetooth stack process** |
| : typically one single process that implements various Bluetooth protocols and |
| profiles above the Host Controller Interface (HCI) and below the Bluetooth |
| SDK APIs. On one hand, it servces the requests from _Application processes_; |
| on the other hand, it forwards these requests via interactions with _HAL |
| processes_. On Android, this process typically runs under AID_BLUETOOTH |
| (usually 1002) with process name "com.android.bluetooth". The process is |
| started in Java and loads native libraries through JNI. Other systems that |
| do not use Java virtual machine may have a pure native process. Multiple |
| threads may exist in this process for various reasons. The GD stack runs |
| entirely in this process. |
| |
| ### Threads in Bluetooth stack process |
| |
| Currently, the goals of thread optimization in the Bluetooth stack are: |
| |
| * Reduce the number of threads as much as possible to simplify synchronization |
| * Do blocking I/O operations in separate threads |
| * Try moving I/O operations into polling mode so that we can use event driven |
| methods to interact with it on main thread |
| * Move alarm and timer mechanisms to their calling threads to avoid a separate |
| alarm thread |
| * Isolate individual components so that each component can be started and |
| stopped individually without terminating the main thread |
| * Prefer data passing over data sharing among threads to reduce locking and |
| race conditions |
| |
| After above optimization, we are left with five main types of threads within the |
| native code: |
| |
| **Main thread** |
| : The main workhorse in the Bluetooth stack. The thread's execution context is |
| further divided into [`Handlers`](#handler) that reside in individual |
| [`Modules`](#module). This thread can be divided further into smaller ones |
| if performance is constrained on the running platform. Deployer just needs |
| to bind handlers to different threads and that should not affect the overall |
| operation. |
| |
| **JNI thread** |
| : In the native thread, we treat the Java layer as a separate application as |
| its threading module is completely different. Therefore, we put a thread |
| between these two layers to buffer any blocking operation. |
| |
| **HCI thread (or other HW I/O thread)** |
| : This thread is responsible for deadling with hardware I/O and can be |
| potentially blocking. Hence it has its separate thread to avoid blocking the |
| main thread. |
| |
| **Audio worker thread** |
| : Responsible for audio encoding and decoding operations that require higher |
| precision execution timing. Such worker has its separate thread to avoid |
| being affected by the main thread. |
| |
| **Socket I/O thread** |
| : Communicate with various applications that uses the |
| [`BluetootSocket`](https://developer.android.com/reference/android/bluetooth/BluetoothSocket) |
| interface. It has its sepearate thread due to potential I/O delay. |
| |
| ### Data flow diagram |
| |
| Function invocations between different components are abstracted as control |
| packets (function closure) passed through queues. Data flow between components |
| are data packets sent through queues, signaled using [`Reactor`](#reactor). They |
| will merge to the input queue for each component. We define three types of |
| queues: |
| |
| **Non-blocking queue** |
| : When users try to dequeue when it’s empty, or enqueue when it’s full, it |
| will return immediately. All queueing within a thread must be non-blocking, |
| because otherwise it will deadlock. |
| |
| **Blocking queue** |
| : When users try to dequeue when it’s empty, or enqueue when it’s full, it |
| will block, until other thread makes the queue to be writable/readable. It |
| can be used as a flow control mechanism to avoid too many packets from user |
| thread. |
| |
| **Leaky queue** |
| : Same as non-blocking queue, but it will flush when it’s full and user tries |
| to enqueue. This is useful for audio encoding. |
| |
|  |
| |
| ## Building blocks |
| |
| ### Module {#module} |
| |
| Code in GD is packed into C++ objects called |
| [`Module`](https://android.googlesource.com/platform/packages/modules/Bluetooth/system/+/master/gd/module.h). |
| A module standardized the following aspects of GD code: |
| |
| * **Dependencies**: A module provides its own dependencies on other modules by |
| implementing `ListDependencies()` |
| * **Life Cycle**: A module must implement `Start()` and `Stop()` life cycle |
| methods |
| * **Threading Module**: The `Module` base class provides a `Handler` for code |
| execution context via `GetHandler()` |
| * **Metrics**: A `Module` can dump its state information for dumpsys through |
| `DumpState()` |
| |
| See its definition at: https://android.googlesource.com/platform/packages/modules/Bluetooth/system/+/master/gd/module.h |
| |
| ### Handler {#handler} |
| |
| Similar to |
| [`android.os.Handler`](https://developer.android.com/reference/android/os/Handler), |
| [`bluetooth::os::Handler`](https://android.googlesource.com/platform/packages/modules/Bluetooth/system/+/master/gd/os/handler.h) |
| provides a sequential execution context while hiding the concept of thread from |
| the executing code. |
| |
| By scoping execution context into smaller areas, `Handler` benefits development |
| in the following ways: |
| |
| * Less need for locking due to sequential execution context |
| * Smaller context leads to easier management of code flow |
| * Separation from thread gives system deployer more freedom to tweak the |
| underlying thread allocation. For example, for real time OS without full |
| thread implementation, a `Handler` can be used to provide a near-thread |
| execution context |
| |
| Of course, there are downsides of using `Handler`, which developers should be |
| cautious about: |
| |
| WARNING: Although multiple `Handler` could bind to the same thread, `Handler` |
| does not gurantee sequential execution of code across different `Handler` even |
| when the are on the same thread. |
| |
| WARNING: Locking among `Handlers` that were bound to the same thread may result |
| in deadlock |
| |
| WARNING: Data must be copied between `Handler` to avoid both deadlock and race |
| condition |
| |
| See its definition at: https://android.googlesource.com/platform/packages/modules/Bluetooth/system/+/master/gd/os/handler.h |
| |
| ### Reactor {#reactor} |
| |
| [`bluetooth::os:Reactor`](https://android.googlesource.com/platform/packages/modules/Bluetooth/system/+/master/gd/os/reactor.h) |
| implements the |
| [Reactor Design Pattern](https://en.wikipedia.org/wiki/Reactor_pattern), in |
| which concurrent _Events_ are demultiplexed by a _Synchronous Event |
| Demultiplexer_ to a list of _Request Handlers_ registered through a |
| _Dispatcher_. |
| |
| In a generic Linux operating system, such as Android, we implemented it using |
| file descriptors such as |
| [eventfd](http://man7.org/linux/man-pages/man2/eventfd.2.html) for `Handler`, |
| [timerfd](http://man7.org/linux/man-pages/man2/timerfd_create.2.html) for |
| `Alarm`, and [socketfd](http://man7.org/linux/man-pages/man2/socket.2.html) for |
| data processing pipelines. In the context of file descriptors, events are |
| catigorized into two types: |
| |
| * **OnReadReady**: means that the demultiplexer has some events for the |
| handler and the handler can read at least one event from the underlying |
| event queue. This is often associated with `EPOLLIN`, `EPOLLHUP`, |
| `EPOLLRDHUP`, and `EPOLLERR`. |
| * **OnWriteReady**: means that the demultiplexer is ready to consume more |
| events from this handler, and the handler can write at least one event to |
| the underlying queue. this is often associated with `EPOLLOUT`. |
| |
| This pattern naturally creates a back pressure from one queue to another without |
| any extra signaling mechanism. When used in networking stack like ours, it |
| simplifies the signaling code flow. |
| |
| See its definition at: |
| https://android.googlesource.com/platform/packages/modules/Bluetooth/system/+/master/gd/os/reactor.h |
| |
| A pure data use case of `Reactor` is a `Reactive Queue`, see its definition at: |
| https://android.googlesource.com/platform/packages/modules/Bluetooth/system/+/master/gd/os/queue.h |
| |
| ## Packet Definition Language (PDL) |
| |
| Packet parsing and serialization has been a big part of any networking stack. It |
| is usually the first snippet of code that interface with a remote device. In the |
| past, this has been achieved manually using macros like `STREAM_TO_UNIT8` or |
| `UINT8_TO_STREAM`. This manual method is tedious and errorprone. To fix this, we |
| created a Packet Definition Language that defines networking packet structure to |
| the bits level. C++ headers and Python bindings will be automatically generated |
| from its code generator and any fixes to the code generator will apply |
| systematically to all packet code generated. |
| |
| Example PDL: |
| |
| ``` |
| // Comments |
| little_endian_packets // Whether this packet is big or small endian |
| |
| // Include header from other C++ header files |
| custom_field SixBytes : 48 "packet/parser/test/" // expect six_bytes.h |
| custom_field Variable "packet/parser/test/" // expect variable.h |
| |
| // A packet |
| packet Parent { |
| _fixed_ = 0x12 : 8, // fixed field 0x12 that takes 8 bits |
| _size_(_payload_) : 8, // Size field that takes 8 bits |
| _payload_, // special payload field of variable size |
| footer : 8, // fiexed size footer of 8 bits |
| } |
| |
| packet Child : Parent { |
| field_name : 16, // addition field append after Parent |
| } |
| |
| // an enum of 4 bits |
| enum FourBits : 4 { |
| ONE = 1, |
| TWO = 2, |
| THREE = 3, |
| FIVE = 5, |
| TEN = 10, |
| LAZY_ME = 15, |
| } |
| ``` |
| |
| See its documentation at: |
| https://android.googlesource.com/platform/packages/modules/Bluetooth/system/+/master/gd/packet/parser/README |
| |
| ## Calling convention between modules |
| |
| ### Asynchronous server-client model |
| |
| For most communication among modules, developers should assume an asynchronous |
| server-client model in a generic model like: |
| |
| ```c++ |
| // Define callback function type |
| using CallbackFunction = std::function<void(ParamType)>; |
| |
| // Asynchronous method definition |
| bool Foo(Parameter param, CallbackFunction callback); |
| |
| // A new callback is passed for each asynchronous call |
| // Always prefer lambda over std::bind |
| CallbackFunction callback = [this] { |
| // do something |
| }; |
| Parameter param = { |
| // something |
| }; |
| if (Foo(param, callback)) { |
| // The callback will be invoked |
| // Callback must be invoked in the future |
| } else { |
| // Failed, no need to wait |
| } |
| ``` |
| |
| Many protocols and profiles fit into such model such as `AclManager` and |
| `L2cap`. |
| |
| ### Synchronous database model |
| |
| In some cases, an asynchronous server-client model is not feasible. In this |
| case, developers can consider a synchronous database model. In such a model, |
| operations can happen synchronously with help of mutex. When the method returns, |
| the changes must be reflected to all dependencies. Any changes in the internal |
| states must be applied atomically. |
| |
| ```c++ |
| // Synchronous method definition |
| void Foo(Parameter param, Output* output); |
| int Bar(Parameter param); |
| Parameter param = { |
| // something |
| }; |
| Output output = {}; |
| Foo(param, &output); |
| // output can be used immediately |
| int bar_output = Bar(param); |
| // bar_output can be used immediately |
| ``` |
| |
| Many storage and informational modules fit into this model such as `Metrics` and |
| `Storage`. |