Interface-Driven Development
Thunder plugins are built around the concept of interfaces. An interface acts as a contract between the plugin and the outside world (this could be external client applications or other plugins).
In Thunder, plugins can expose their interfaces over two communication channels:
- JSON-RPC (https://www.jsonrpc.org/specification)
- COM-RPC (Custom Thunder RPC protocol)
In older Thunder versions (<R4), it was common for plugins to define two interface files, one for each RPC format. In Thunder R4 and later, it is strongly recommended to use the code-generation tools and only write a single interface file. This makes it much easier to maintain and reduces the amount of boilerplate code that must be written to create a plugin. This is the approach documented in this page.
Examples of existing Thunder interfaces can be found in the ThunderInterfaces repository: https://github.com/rdkcentral/ThunderInterfaces/.
Designing a good Interface
Before writing any plugin code, it is essential to define the interface that plugin will implement. A good interface defines a set of methods that a plugin can support, without dictating anything about the actual code that will implement the interface. It is possible that many plugins could implement that same interface if they provide overlapping or equivalent functionality (for example an IBrowser
interface could be implemented by different web browser engines such as WebKit or Chromium).
The interface provides a clear boundary between the code that invokes methods on the plugin and the plugin code that implements the functionality. Typically, interface definitions are stored in a separate repository to the plugin implementations to reflect this boundary.
Well-designed interfaces should also be easy to read and self explanatory. Methods names should be descriptive and have doxygen-style comments that describe their functionality, inputs and outputs. All methods should return a standardised error code to indicate whether they completed successfully, and any data that should be returned by the method should be stored in an output parameter.
It is possible to build an interface from smaller sub-interfaces using composition. This is preferred to having a large monolithic interface, as it encourages reuse and modularity.
Interface Definitions
COM-RPC Interfaces
When designing a plugin interface, it is always best to start from the COM-RPC interface definition. A COM-RPC interface is defined in a C++ header file, and is a struct that inherits virtually from Core::IUnknown
. This is very similar to interfaces in Microsoft COM. The plugin code will then provide an implementation of one or more COM-RPC interfaces.
During the build, code-generation tools automatically generate ProxyStub classes for each interface. These ProxyStubs handle the serialisation/deserialisation of messages when crossing process boundaries (this code is not necessary for in-process COM-RPC communication, since that just resolves down to local function calls and no data marshalling occurs).
IUnknown
All COM-RPC interfaces inherit virtually from IUnknown
. As with Microsoft COM, this contains 3 vital methods:
- QueryInterface() - provides interface navigation. Allows clients to dynamically discover (at run time) whether or not an interface is supported. Given an interface ID, if the plugin supports that interface than a pointer to that interface will be returned, otherwise will return nullptr.
- AddRef() - lifetime management. Increase the reference count on the object
- Release() - lifetime management. Decrement the reference count on the object. When 0, it is safe to destroy the object
Interface Characteristics
- Interfaces are not plugins: A Thunder plugin can implement 0 or more COM-RPC interfaces, but an interface cannot be instantiated by itself because it does not define an implementation. It is possible for many Thunder plugins to implement the same interface
- COM-RPC clients interact with pointers to interfaces: A client application communicating with a Thunder plugin over COM-RPC will receive nothing more than an opaque pointer through which it can access the interface methods. It cannot access any data from the plugin that implements the interface. This encapsulation ensures the client can only communicate with the plugin over an agreed interface
- Interfaces are immutable: COM-RPC interfaces are not versioned, so if an interface needs changing it must only add new methods. It must never break existing methods
- Interfaces are strongly typed: Each COM-RPC interface has a unique ID, used to identify it in the system. When a client requests a pointer to an interface, it does so using the ID of that interface.
Guidelines
-
Each COM-RPC interface must inherit virtually from
Core::IUnknown
and have a unique ID- Virtual inheritance is important to prevent the diamond problem and ensure multiple interfaces are implemented on a single, reference counted object. Without this, each interface might have its own reference count and not be destroyed correctly.
-
Ensure API compatibility is maintained when updating interfaces to avoid breaking consumers
- Methods should be pure virtual methods that can be overridden by the plugin that implements the interface
- Methods should return
Core::hresult
which will store the error code from the method- If a method succeeds, it should return
Core::ERROR_NONE
- If a method fails, it should return a suitable error code that reflects the failure (e.g
Core::ERROR_READ_ERROR
) - If an error occurs over the COM-RPC transport the
COM_ERROR
bit will be set. This allows consumers to determine where the failure occurred
- If a method succeeds, it should return
-
Ensure all enums have explicit data types set (if not set, the code generators will fall back to
uint32_t
as a safe default)- If you know you will be communicating over different architectures, then you can define
INSTANCE_ID_BITS
to specify a specific default integer width
- If you know you will be communicating over different architectures, then you can define
-
C++ types such as
std::vector
andstd::map
are not compatible with COM-RPC- Only "plain-old data" (POD) types can be used (e.g. scalar values, interface pointers)
- COM-RPC can auto-generate iterators for returning multiple results
- Ensure integer widths are explicitly set (e.g. use
uint16_t
oruint8_t
instead of justint
) to prevent issues if crossing architecture boundaries - Prefer asynchronous APIs for long running tasks (and use notifications to signal completion)
Notifications & Sinks
A COM-RPC interface not only allows for defining the methods exposed by a plugin, but can also be used to define notifications that plugins can raise and clients can subscribe to.
As with Microsoft COM, this is done by allowing clients to create implementations of notification interfaces as sinks, and register that sink with the plugin. When a notification occurs, the plugin will call the methods on the provided sink.
JSON-RPC Interfaces
Once a COM-RPC interface is defined, if a JSON-RPC interface is also required then the interface should have the @json
tag added. This signals to the code-generator that a corresponding JSON-RPC interface should be generated alongside the COM-RPC one.
By default, when the @json
tag is added, all methods in the COM-RPC interface will have corresponding JSON-RPC methods. It is possible to ignore/skip specific methods from the JSON-RPC generation by adding tags in the interface definition.
In older Thunder versions (<R4), JSON-RPC interfaces were defined using separate JSON schema files. These would then need to be manually wired up in the plugin. By using the code-generators, we can eliminate this step, making it much faster and easier to write plugins. It is no longer recommended to create JSON schema files for JSON-RPC interfaces.
Overview
-
It is now possible to specify a separate JSON-RPC interface for handling connection issues and correct session management, and this will bring more options when the JSON-RPC interface deviates from the COM-RPC interface.
-
The JSON-RPC generator now supports the usage of nested "plain-old data"(POD) types (such as plain C structs) in the C++ interface.
- The JSON-RPC generator can now parse these structs and their usage in methods and generate a JSON-RPC interface for such cases.
-
Core::hresult
is now supported as a return type from Thunder 5.0 onwards for JSONRPC and is strongly recommended to be used. -
@text
metatag has been extended to have more options to influence the names used in generated code.- For more details click here.
-
Float type is now supported in the IDL header files.
-
Fixed size arrays are now supported, for example:
array[10]
- See an example in ICryptography
-
Core::instance_id
is now supported in the IDL header files.- It is presented as a 32/64 bit hexadecimal value in JSON-RPC.
-
Core::Time
is now supported in the IDL header files.- It is presented as an ISO 8601 formatted string in JSON-RPC.
-
Core::OptionalType<T>
allows a member to be optional (this superseded @optional), and must be used if an attribute is expected to be optional on JSON-RPC. In COM-RPC the OptionalType can be used to see if a value was set, and in JSON-RPC it is then allowed to omit this parameter.- A @default tag can be used to provide a value, in the case T is not set. See more here.
- Note: Currently, OptionalType does not work with C-Style arrays.
-
JSON-RPC supports the usage of bitmask enums (a combination of enum values can be set in the same enum parameter at the same time).
- This is mapped as an array of values in the JSON-RPC interface.
- See more information about
/* @encode:bitmask */
here.
Preventing Memory leaks
A resource allocated by a remote client must still be freed in case the channel is disconnected before the client is able to do it on its own.
To deal with this, a method can receive a Core::JSONRPC::Context
as a parameter.
Amongst other things, the context includes the channel ID, which enables the association of the JSON-RPC call with the client.
Note: Context must be defined as the first parameter and will not be visible in the JSON-RPC messages.
virtual Core::hresult Join(const Core::JSONRPC::Context& context,...) = 0;
IConnection::INotification
can be used to be notified of the dropped channels.
Examples:
View Messenger.h to see how Core::JSONRPC::Context
is used.
Notification Registration
Notification registration is a way of tracking updates on a notification.
Tagging a notification with @statuslistener will emit additional code that will allow you to be notified when a JSON-RPC client has registered (or unregistered) from this notification. As a result, an additional IHandler interface is generated, providing the callbacks.
Examples:
In IMessenger.H, @statuslistener is used on two methods.
This example will demonstrate the ability to be notified when a user performs a certain action.
Suppose an interface "INotification" that contains a method "RoomUpdate", which tracks the availability of a room. The method is tagged with @statuslistener, which will allow for the creation of an "IHandler" interface. The "IHandler" interface will contain the required declaration of methods to allow for notification registration tracking.
// @json 1.0.0
struct EXTERNAL IMessenger {
virtual ~IMessenger() = default;
/* @event */
struct EXTERNAL INotification {
virtual ~INotification() = default;
// @statuslistener
virtual void RoomUpdate(...) = 0;
}
}
struct IHandler {
virtual ~IHandler() = default;
virtual void OnRoomUpdateEventRegistration(const string& client, const
PluginHost::JSONRPCSupportsEventStatus::Status status) = 0;
}
Using the "IHandler" interface, its methods should be implemented to track the availability of the room.
class Messenger : public PluginHost::IPlugin
, public JSONRPC::IMessenger
, public JSONRPC::JMessenger::IHandler {
// JSONRPC::JMessenger::IHandler override
void OnRoomUpdateEventRegistration(const string& client, const
PluginHost::JSONRPCSupportsEventStatus::Status status) {
if(status == Status::registered) {
for (const string& room: _rooms) {
JMessenger::Event::RoomUpdate(...)
}
}
}
}
Object lookup
Object lookup defines the ability to create a JSON-RPC interface to access dynamically created objects (or sessions). This object interface is brought into JSON-RPC scope with a prefix.
The format for this ability is to use the tag '@lookup:[prefix]' on a method.
Note: If 'prefix' is not set, then the name of the object interface is used instead.
Example
The signature of a lookup function must be:
virtual <Interface>* Method(const uint32_t id) = 0;
An example of an IPlayer interface containing a playback session. Using tag @lookup, you are able to have multiple sessions, which can be differentiated by using JSON-RPC.
struct IPlayer {
struct IPlaybackSession {
virtual Core::hresult Play() = 0;
virtual Core::hresult Stop() = 0;
};
// @lookup:session
virtual IPlaySession* Session(const uint32_t id) = 0;
virtual Core::hresult Create(uint32_t& id /* @out */) = 0;
virtual Core::hresult Configure(const string& config) = 0;
}
Player.1.configure
Player.1.create
Player.1.session#1::play
Player.1.session#1::stop
Code Generation
The code generation tooling for Thunder lives in the ThunderTools repository. These tools are responsible for the generation of ProxyStub implementations for COM-RPC, JSON-RPC interfaces, JSON data types, and documentation.
When building the interfaces from the ThunderInterfaces repository, the code generation is automatically triggered as part of the CMake configuration.
[cmake] -- ProxyStubGenerator ready /home/stephen.foulds/Thunder/host-tools/sbin/ProxyStubGenerator/StubGenerator.py
[cmake] ProxyStubGenerator: IAVNClient.h: created file ProxyStubs_AVNClient.cpp
[cmake] ProxyStubGenerator: IAVSClient.h: created file ProxyStubs_AVSClient.cpp
[cmake] ProxyStubGenerator: IAmazonPrime.h: created file ProxyStubs_AmazonPrime.cpp
[cmake] ProxyStubGenerator: IApplication.h: created file ProxyStubs_Application.cpp
...
Each interface definition will result in up to 4 auto-generated files depending on whether a JSON-RPC interface is required.
ProxyStubs_<Interface>.cpp
- COM-RPC marshalling/unmarshalling codeJ<Interface>.h
- If the@json
tag is set, this will contain boilerplate code for wiring up the JSON-RPC interface automaticallyJsonData_<Interface>.h
- If the@json
tag is set, contains C++ classes for the serialising/deserialising JSON-RPC parametersJsonEnum_<Interface>.cpp
- If the@json
tag is set, and the interface contains enums, this contains code to convert between strings and enum values
The resulting generated code is then compiled into 2 libraries:
-
/usr/lib/thunder/proxystubs/libThunderMarshalling.so
- This contains all the generated proxy stub code responsible for handling COM-RPC serialisation/deserialisation
-
/usr/lib/thunder/libThunderDefinitions.so
- This contains all generated data types (e.g. json enums and conversions) that can be used by plugins
Note
There will also be a library called libThunderProxyStubs.so
installed in the proxystubs
directory as part of the main Thunder build - this contains the generated ProxyStubs for the internal Thunder interfaces (such as Controller and Dispatcher).
The installation path can be changed providing the proxystubpath
option in the Thunder config.json file is updated accordingly so Thunder can find the libraries. When Thunder starts, it will load all available libraries in the proxystubpath
directory. If an observable proxystubpath is set in the config, then Thunder will monitor that directory and automatically load any new libraries in that directory. This makes it possible to load new interfaces at runtime.
If you are building the interface as part of a standalone repository instead of ThunderInterfaces, it is possible to manually invoke the code generation tools from that repository's CMake file. The CMake commands drive the following Python scripts:
- ThunderTools/ProxyStubGenerator/StubGenerator.py
- ThunderTools/JsonGenerator/JsonGenerator.py
The below CMakeLists.txt file is an example of how to invoke the code generators, you may need to tweak the build/install steps according to your specific project requirements
CMakeLists.txt | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
|
Worked Example
Tip
This example only focuses on defining an interface. Writing the plugin that implements an interface is beyond the scope of this example. Refer to the "Hello World" walkthrough elsewhere in the documentation for a full end-to-end example.
For this example, we will define a simple interface that defines a way to search for WiFi access points (APs) and connect to them.
Our WiFi interface should define 3 methods:
-
Scan - start a scan for nearby APs. This will be an asynchronous method since a WiFi scan could take some time
- This will take no arguments
-
Connect - connect to an AP discovered during the scan
- This will take one argument - the SSID to connect to
-
Disconnect - disconnect from the currently connected AP
- This will take no arguments
It should also define one notification/event that will be fired when the scan is completed. This notification will contain the list of APs discovered during the scan.
As with C++, COM-RPC interfaces are prefixed with the letter "I". Therefore the name of this example interface will be IWiFi
.
Define Interface ID
Warning
Ensure your interface has a unique ID! If the ID is in use by another interface it will be impossible for Thunder to distinguish between them.
All interfaces must have a unique ID number that must never change for the life of the interface. From this ID, Thunder can identify which ProxyStub is needed to communicate over a process boundary.
Each ID is a uint32_t
value. This was chosen to reduce the complexity and minimise the size of the data on the wire when compared to a GUID.
Interface IDs for the core Thunder interfaces (such as Controller) are defined in the Thunder source code at Source/com/Ids.h
. For existing Thunder interfaces in the ThunderInterfaces repository, IDs are defined in the interfaces/Ids.h
file.
For this example we will define 3 unique IDs for our interface represented by the following enum values:
- ID_WIFI
- ID_WIFI_NOTIFICATION
- ID_WIFI_AP_ITERATOR
Define COM-RPC Interface
Each COM-RPC interface should be defined in a C++ header file with the same name as the interface, so in this case IWiFi.h
IWiFi.h | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
|
- Use the include tag to include the COM-RPC iterator code
- Each interface must have a unique ID value
- We need to return a list of detected access points. Since we can't use a standard container such as
std::vector
, use the supported COM-RPC iterators. This iterator must have a unique ID - All interfaces must have a unique ID, so the
INotification
interface must also have an ID - This method will be invoked when the AP scan completes, and the
accessPoints
variable will hold a list of all the discovered APs. - Provide register/unregister methods for COM-RPC clients to subscribe to notifications
Enable JSON-RPC Generation
To enable the generation of the corresponding JSON-RPC interface, add the @json tag above the interface definition.
Here, the @json
tag was passed a version number 1.0.0
, which can be used to version JSON-RPC interfaces. If not specified, it will default to 1.0.0
IWiFi.h | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
When JSON-RPC support is enabled, the code generator will create code to for the plugin to register the JSON-RPC methods, and code to convert between JSON and C++ classes.
Danger
The below code samples are auto-generated and provided as an example. As the code-generation tools change, the actual output you see may look different than the below. Do not copy the below code for your own use
The generated JWiFi.h
file contains two methods - Register
and Unregister
, which are used by the plugin to connect the JSON-RPC interface to the underlying implementation.
Auto-generated code (click to expand/collapse)
JWiFi.h | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
|
The auto-generated JsonData_WiFi.h
file contains code that can convert from the parameters object in the incoming JSON-RPC request to a C++ object. This is used by both the plugin and client apps to read incoming parameters and build responses.
Auto-generated code (click to expand/collapse)
JsonData_WiFi.h | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
|
Since there are no enums in this example interface, no JsonEnums_WiFi.cpp
file was generated.