Skip to content

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:

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
  • 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
  • C++ types such as std::vector and std::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 or uint8_t instead of just int) 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 required 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]

  • 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::MACAddress is also fully supported

  • 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.
    • If a parameter is not optional now in the generated documentation for this interface that parameter will now be explicitly mentioned to be mandatory to prevent any confusion and indicate it must be provided when using the JSON-RPC interface.
  • 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.
  • The usage of std::vector< Type > is now supported by the Proxy Stub generators as well as the JSON-RPC generators (where it translates to a json array).

    • nested usage of std::vector is not supported, meaning a std::vector< std::vector< Type > >. Of course a std::vector inside a struct where the struct is used as type in another std::vector is.
    • @restrict is mandatory to be used to indicate the maximum allowed size of the std::vector (to make the interface designed think about size consequences and not allow unlimited sized vectors). For COM-RPC it means all the data in the std::vector is transferred at once, if this is not desired best to use an iterator as an alternative.
    • usage of std::vector is also supported in notifications (JSON-RPC event). Please use judiciously, see the interface guidelines section on why that is (e.g. here and here).
    • a std::vector cannot contain a Core::OptionalType (at least not for the code generators) as that would not make sense. Then just do not add the optional elements to the vector.
  • With the generically available "versions" function a client can query what versions of what JSON-RPC interfaces a plugin implements. For more info see here


Checking if a function exists

With the generically available "exists" function a client can query if a specific function is available on a given plugin JSON-RPC interface. Call it like

{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "MyPlugin.1.exists",
  "params": {
    "method": "methodname"
  }
}

it will return true or false to indicate whether the method was available or not:

{
  "jsonrpc": "2.0",
  "id": 42,
  "result": true
}

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;
Note: IConnection::INotification can be used to be notified of the dropped channels.

Examples:

View Messenger.h to see how Core::JSONRPC::Context is used.

Note in newer Thunder versions session support was added which in a lot of cases automates this and leaks are prevented automatically, for more info see the object lookup section and the custom object lookup


Notifications

Notification registration is a way of tracking updates on a notification.

When a notification is tagged with the @event keyword it means code will be generated to make it easy to send this notification to registered clients from the C++ code that implements the interface. Registering for a JSON-RPC event can be done by the client by calling the "register" function which is generically available for all plugins (if documentation is generated for an interface or plugin using that interface containing events, examples for registration and deregistration and the notification itself will be included in the documentation):

{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "MyPlugin.1.register",
  "params": {
    "event": "eventname",
    "id": "myapp"
  }
}

if the registration is successful the following return can be expected (and an error response in case the registration failed) :

{
  "jsonrpc": "2.0",
  "id": 42,
  "result": null
}

Unregistering from an event can be done by the client by calling the "unregister" function, which like the register is generically available for all plugins:

{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "MyPlugin.1.unregister",
  "params": {
    "event": "eventname",
    "id": "myapp"
  }
}

if the deregistration is successful the following return can be expected (and an error response in case the deregistration failed) :

{
  "jsonrpc": "2.0",
  "id": 42,
  "result": null
}

In the generated J< interface >.h file static helper functions will be generated that one can call in the C++ code to send the notifications to all registered clients. For example suppose we would have this interface:

    // @json 1.0.0
    struct EXTERNAL ITest : virtual public Core::IUnknown {

        enum { ID = ID_TEST };

        // @event
        struct EXTERNAL INotification : virtual public Core::IUnknown {

            enum { ID = RPC::ID_TEST_NOTIFICATION };

            virtual void StateChange(const string& callsign, const bool test) = 0;
        };

        virtual Core::hresult Register(INotification* const sink) = 0;
        virtual Core::hresult Unregister(const INotification* const sink) = 0;

        virtual Core::hresult Test() const = 0;
    };

After including the generated helper file one could notify all registered clients on a StateNotification class by calling the following code:

        Exchange::JTest::Event::StateChange(*this, "test", true);

Note depending on the interface more than one helper function is generated (e.g. also one where you can use the generated params class), check the generated J< interface >.h file for all the options.

indexed events

With the above and the generated event notification functions the notification will be sent to all registered clients. It is possible to only notify some clients register for an event. If one wants to send some notifications only to some registered clients there must be a parameter in the event that a client uses to indicate if it wants to only receive the notification when that parameter has a certain value. That parameter can be flagged with the @index tag to indicate it is used to discriminate registered clients.

Let's discuss with an example:

Suppose we have this interface:

    // @json 1.0.0
    struct EXTERNAL ITest : virtual public Core::IUnknown {

        enum { ID = ID_TEST };

        // @event
        struct EXTERNAL INotification : virtual public Core::IUnknown {

            enum { ID = RPC::ID_TEST_NOTIFICATION };

            virtual void StateChange(const string& callsign, const bool test) = 0;
        };

        virtual Core::hresult Register(INotification* const sink, const string& callsign /* index */) = 0;
        virtual Core::hresult Unregister(const INotification* const sink, const string& callsign /* index */) = 0;

        virtual Core::hresult Test() const = 0;
    };

From the COM-RPC interface it is clear one can subscribe to be notified on changes for a specific callsign. The @index is put at the COM-RPC Register and Unregister methods, the code generators will do a name lookup, so in this case "callsign" to find the connected event. (the event can have the @index tag as well, as long as the index parameter name matches).

A client can subscribe via JSON-RPC to be notified on a specific callsign like this:

{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "<plugin>.1.register@<callsign>",
  "params": {
    "event": "stateChange",
    "id": "myid"
  }
}

where < callsign > should have the specific callsign this client wants to be notified about.

The notification sent will then be like this:

{
  "jsonrpc": "2.0",
  "method": "myid.stateChange@<callsign>",
  "params": {
    "test": false
  }
}

As with a non indexed event if the documentation is generated for the JSON-RPC interface examples for registering, deregistering and the notification will be included in the document.

Again static helpers are created in the J< interface >.h file that allow one to sent the notifications to all registered clients:

        Exchange::JTest::Event::StateChange(*this, "test", true);

This will only sent a notification to the clients who registered for the stateChange event with client "test" by adding @test to the registration designator.

This generated static helper uses a lambda internally which will send the notification to all clients who registered for the index passed to the helper function. It is also possible to pass a lambda yourself as last parameter to the static helper function when you want different behaviour.

It is also possible to use Core::OptionalType to indicate the index is not mandatory (and if not set one registers for all values of the index).

    // @json 1.0.0
    struct EXTERNAL ITest : virtual public Core::IUnknown {

        enum { ID = ID_TEST };

        // @event
        struct EXTERNAL INotification : virtual public Core::IUnknown {

            enum { ID = RPC::ID_TEST_NOTIFICATION };

            virtual void StateChange(const string& callsign, const bool test) = 0;
        };

        virtual Core::hresult Register(INotification* sink, const Core::OptionalType<string>& callsign /* @index */) = 0;
        virtual Core::hresult Unregister(INotification* sink, const Core::OptionalType<string>& callsign /* @index */) = 0;

        virtual Core::hresult Test() const = 0;
    };

for the JSON-RPC registration that does not influence much, one uses:

{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "<plugin>.1.register@<callsign>",
  "params": {
    "event": "stateChange",
    "id": "myid"
  }
}

to register only to be notified for a specific callsign and:

{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "<plugin>.1.register",
  "params": {
    "event": "stateChange",
    "id": "myid"
  }
}

to register for all callsigns no matter what the value of callsign is.

The notification is now a little different however:

{
  "jsonrpc": "2.0",
  "method": "myid.stateChange@abc",
  "params": {
    "callsign": "...",
    "test": false
  }
}
It will now always include the callsign as parameter (as if the event was for a client that registered for all callsigns the @ part will not be in the event so the callsign must be included as parameter to indicate for which callsign this event is).

As always these examples will also be included in the generated documentation for this interface

And again static helper functions will be included in the generated J< interface >.h file for sending the notifications.

statuslisteners

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. Note: the 'unregistered' notification is also triggered if the client disconnects (channel closed) without explicitly calling Unregister beforehand.

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;
    }
}   
An example of a generated IHandler interface providing the callbacks from the RoomUpdate() function.

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(...)
                }
            }
    }
}
For a more detailed view, visit Messenger.h.


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 object ID. This translates the Object Oriented domain (used in COM-RPC interfaces) to the functional domain (JSON-RPC).

Object lookup will happen automatically when the @encode:autolookup tag is added to the interface that is to be the session object. The generator will look for methods return the session type as an out parameter as COM-RPC Interface (where the interface must in the same file and of course also must be tagged with @json) and it is expected that there is also a method that takes the same interface as input parameter to be available as well to be able to destroy the created object.

The generated JSON-RPC interface will then automatically (therefore the lookup method also described as auto object lookup) associate the method with the interface out parameter as a creation function for an object that implements the interface of the out parameter and will return an object ID for JSON-RPC to identify the created object. This object ID can then be used in subsequent calls on methods available on the type of the interface to indicate the object you want the function to be called upon. the JSON-RPC generator associates the COM-RPC method with the input interface pointer as the method that will destroy the created object, on JSON-RPC level the object ID to destroy is expected as an input parameter. In the JSON-RPC interface the object-ID will then for the function that creates the object be returned as output parameter and for functions to be called on an object the method for the JSON-RPC call should be the name of the interface (to prevent name clashes) and after that add "#< device is >::< object method name >" to specify the method to be called on the object and the object ID on which the method should be called on. But easier is to just generate the documentation for this interface with the document generator as that will include examples for all methods.

Note this also works when the interface contains an event. You will then be able to register specifically for the events of a specific object ID and only receive the ones generated for that object ID.

The calls using a certain object ID must be done on the same channel the object ID was created on. To prevent any object leaks a call must be made into the generated code to release any objects still held when the channel closes (in case the destroy function was not called before the channel terminated). See example below on how to do this. It is also possible to pass a callback to this generated method so you are notified on the objects being destroyed, in case you need to trigger specific code for an object being destroyed.

Meaning to be able to use this COM-RPC interface in JSON-RPC no additional code needs to be written, it is enough to implement the COM-RPC interface and connect the JSON-RPC interface to it as you would normally do, the code generator will take care of the full JSON-RPC interface.

It is also possible to register callbacks to be called when objects are acquired or relinquished in case special handling is needed for JSON-RPC (generic code can be put into the COM-RPC code for Acquire and Relinquish as that is called for both cases).

Note that for the above to work the plugin should derive from the PluginHost::JSONRPCSupportsAutoObjectLookup class instead of the PluginHost::JSONRPC class for its PluginHost::IDispatcher implementation.

Example

here you will see an example of an interface that uses automatic object lookup.

The Acquire method on COM-RPC creates an object of type IDevice which has the @encode:autolookup tag specified. With the Relinquish method the object is destroyed again.

If you look into the generated documentation for this interface which can be found here

The acquire function will now return an object id for the IDevice object created.

If you now want a function/property to call on that specific IDevice object you specify the object id in the designator using the #delimiter, see here on how to do that.

If you are done using the object you can call the relinquish method specifying the object ID of the object you want to destroy.

The IDevice also has an event, see here.

To register for an event for a specific Device, so object ID in the JSON-RPC world you can see here how that is done, again specify the object ID with the # delimiter in the designator for the register method. As you can see when the event is sent the object ID is also in the event designator (convenient in case you registered for notifications of multiple object ID's)

Here you can see how to call the generated Closed method to do cleanup in case the channel closed without all devices being Relinquished. As mentioned it is also possible to pass a callback to the Closed method to get notified on all devices being released here (not demonstrated).

Here is an example of callbacks registered to be called when a JSON-RPC acquire or relinquish request is being handled.


Custom Object lookup

As an alternative to "auto object lookup" where the object-id creation and linking to an object is automatically handled for you under the hood there is also a "custom object lookup" feature. This can be used when an object already has a logical instance-id itself that can uniquely be used to identify the object (e.g. each object has a unique name of number id). Now that logical id can also be exposed to the JSON-RPC world to connect a request to a certain object. One is now self responsible for handing out the id's and connecting them to the object as well as the object lifetime.

To enable custom lookup add the @encode:lookup to the interface representing the object. The code generator will now look for functions or properties where the object interface is returned as output parameter and assume that function is used to retrieve the object by its id (which should be the other parameter of the function or property). Two lambdas should be registered in the plugin code that provide the translation from object to object ID and vice versa.

Like for autolookup (indexed) events are fully supported for custom lookup.

Handling of the object ID's in the JSON-RPC interface is exactly the same as for autolookup, but again easiest is to just generate the documentation for this interface with the document generator as that will include examples for all methods.

Lifetime for the objects must be handled in the implementation of the interface (this can now not be automatic as the generated code does not have any internal storage for the objects). If the objects do have a static lifetime (meaning their lifetime is equal to the plugin) this is easy and no "relinquish" code or leakage prevention is needed. If the objects lifetime is dynamic (meaning they are created and destroyed on the fly) this is more complicated. The COM-RPC method that returns the object by name can now create the object, a "relinquish" method must be added as well, just like for autlookup, as one can not use the COM-RPC "Release" method as that is not exposed to JSON-RPC. And one should probably also want to register for PluginHost::IShell::IConnectionServer::INotification notifications to be able to cleanup objects that were not explicitly relinquished before the channel closed.

If the objects can only be used on a certain channel is up to the implementation. If they are static most likely not, if they are dynamic they most probably will. But this can be completely handled in the implementation as the object ID <-> object lambdas have full access to the JSON-RPC context.

Note that for the above to work the plugin should derive from the PluginHost::JSONRPCSupportsObjectLookup class instead of the PluginHost::JSONRPC class for its PluginHost::IDispatcher implementation.

Example

This example uses static objects, therefore no special lifetime handling is required. Also the usage of the objects is not restricted to specific channels.

here you will see an example of an interface that uses custom object lookup.

With the @encode:lookup on the IAccessory interface we indicated this interface is used as an object.

The Accessory property on the ISimpleCustomObjects interface allows access to a specific Accessory object by specifying the accessory name.

The Accessory functions or properties like Pin now can be accessed when using JSON-RPC requests passing the object ID retrieved with the Accessory property.

The generated documentation for this interface can be found here The Accessory documentation shows that one can retrieve the object ID for an accessory by passing it as index to the property. The property will return the object ID as return value. Calling for example Pin can be done by calling the "accessory" method followed by #< object ID>::pin, and as the pin is an indexed property the index follows after that with a @ delimiter.

The plugin code can be found here

Here we see the plugin derives from PluginHost::JSONRPCSupportsObjectLookup. And here we see the lambdas being registered that take care of the object ID to object conversion and vice versa.

Asynchronous Functions

When an action triggered by a method on a COM-RPC interface which takes some time to complete this method will be made asynchronous, meaning it will return when the action is started and it will expect a callback interface to be passed as input parameter to the method so a method on the callback interface will be called when the action that was started is finished or failed (it it mandatory that the started action always results in a method to be called so it is clear when the action is over).

This is also supported on the JSON-RPC interface. Of course it is not possible to pass a callback so here the end of the action will be indicated by an event send on the same channel the action was started on and an async event ID will be used to connect the function that started the action to the event. Note that you do not explicitly have to register for the event, it will be automatically sent on the channel the action was started on. Use the @async tag to indicate the method should be async on the JSON-RPC interface (it is of course expected to have one input callback interface parameter and the interface for the callback should contain one callback method only). See the example below for more information.

Again meaning to be able to use this COM-RPC interface in JSON-RPC no additional code needs to be written, it is enough to implement the COM-RPC interface and connect the JSON-RPC interface to it as you would normally do, the code generator will take care of the full JSON-RPC interface.

Example

here you will see an interface that has a COM-RPC method that triggers an asynchronous event. The method Connect allows to pass an ICallback object that will be used to notify when the asynchronous event has completed. Complete will be called on the passed ICallback object to indicate this. async is set for this method to indicate we also want the JSON-RPC function to be async.

In the generated documentation for the interface it can be seen how this works on JSON-RPC, found here The connect function now is expected to also pass an async ID as parameter. The connect function will result in a response on the JSON-RPC request to indicate the action was triggered but when the action is completed an event is sent on the same channel that started the action which includes the async ID and the name of the method in the designator.

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 code
  • J<Interface>.h - If the @json tag is set, this will contain boilerplate code for wiring up the JSON-RPC interface automatically
  • JsonData_<Interface>.h - If the @json tag is set, contains C++ classes for the serialising/deserialising JSON-RPC parameters
  • JsonEnum_<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
project(SampleInterface)

find_package(Thunder)
find_package(${NAMESPACE}Core REQUIRED)
find_package(${NAMESPACE}COM REQUIRED)
find_package(CompileSettingsDebug REQUIRED)
find_package(ProxyStubGenerator REQUIRED)
find_package(JsonGenerator REQUIRED)

# Interfaces we want to build
set(INTERFACES_HEADERS
    "${CMAKE_CURRENT_SOURCE_DIR}/ITest.h"
)

# Invoke the code generators
ProxyStubGenerator(
    INPUT ${INTERFACES_HEADERS}
    OUTDIR "${CMAKE_CURRENT_BINARY_DIR}/generated"
)

JsonGenerator(
    CODE
    INPUT ${INTERFACES_HEADERS}
    OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/generated"
)

# Find the generated sources
file(GLOB PROXY_STUB_SOURCES "${CMAKE_CURRENT_BINARY_DIR}/generated/ProxyStubs*.cpp")
file(GLOB JSON_ENUM_SOURCES "${CMAKE_CURRENT_BINARY_DIR}/generated/JsonEnum*.cpp")
file(GLOB JSON_HEADERS "${CMAKE_CURRENT_BINARY_DIR}/generated/J*.h")

# Build the proxystub lib
add_library(ExampleProxyStubs SHARED
    ${PROXY_STUB_SOURCES}
    Module.cpp
)

target_include_directories(ExampleProxyStubs
    PRIVATE
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
)

target_link_libraries(ExampleProxyStubs
    PRIVATE
    ${NAMESPACE}Core::${NAMESPACE}Core
    CompileSettingsDebug::CompileSettingsDebug
)

# Build the definitions lib if applicable
if(JSON_ENUM_SOURCES)
    add_library(ExampleDefinitions SHARED
        ${JSON_ENUM_SOURCES}
    )

    target_link_libraries(ExampleDefinitions
        PRIVATE
        ${NAMESPACE}Core::${NAMESPACE}Core
        CompileSettingsDebug::CompileSettingsDebug
    )
endif()

# Install libs & headers
string(TOLOWER ${NAMESPACE} NAMESPACE_LIB)
install(TARGETS ExampleProxyStubs
    EXPORT ExampleProxyStubsTargets
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/${NAMESPACE_LIB}/proxystubs COMPONENT ${NAMESPACE}_Runtime
)

if(JSON_ENUM_SOURCES)
    install(TARGETS ExampleDefinitions
        EXPORT ExampleDefinitionsTargets
        LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/ COMPONENT ${NAMESPACE}_Runtime
    )
endif()

install(FILES ${INTERFACES_HEADERS}
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${NAMESPACE}/interfaces
)

install(FILES ${JSON_HEADERS}
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${NAMESPACE}/interfaces/json
)

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
#pragma once
#include "Module.h"

// @stubgen:include <com/IIteratorType.h> // (1)

namespace Thunder {
namespace Exchange {
    struct EXTERNAL IWiFi : virtual public Core::IUnknown {
        enum {
            ID = ID_WIFI // (2)
        };

        /**
         * @brief Represent a single WiFi access point
         */
        struct AccessPoint {
            string ssid;
            uint8_t channel;
            uint32_t frequency;
            int32_t signal;
        };
        using IAccessPointIterator = RPC::IIteratorType<AccessPoint, ID_WIFI_AP_ITERATOR>; // (3)

        /* @event */
        struct EXTERNAL INotification : virtual public Core::IUnknown {
            enum {
                ID = ID_WIFI_NOTIFICATION // (4)
            };

            /**
             * @brief Signal that a previously requested WiFi AP scan has completed
             */
            virtual void ScanComplete(IAccessPointIterator* accessPoints /* @in */) = 0; // (5)
        };

        /**
         * @brief Start a WiFi scan
         */
        virtual Core::hresult Scan() = 0;

        /**
         * @brief Connect to an access point
         *
         * @param   ssid      SSID to connect to
         */
        virtual Core::hresult Connect(const string& ssid) = 0;

        /**
         * @brief Disconnect from the currently connected access point
         */
        virtual Core::hresult Disconnect() = 0;

        /**
         *  @brief Register for COM-RPC notifications
         */
        virtual uint32_t Register(IWiFi::INotification* notification) = 0; // (6)

        /**
         * @brief Unregister for COM-RPC notifications
         */
        virtual uint32_t Unregister(const IWiFi::INotification* notification) = 0;
    };
}
}
  1. Use the include tag to include the COM-RPC iterator code
  2. Each interface must have a unique ID value
  3. 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
  4. All interfaces must have a unique ID, so the INotification interface must also have an ID
  5. This method will be invoked when the AP scan completes, and the accessPoints variable will hold a list of all the discovered APs.
  6. 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
#pragma once
#include "Module.h"

// @stubgen:include <com/IIteratorType.h>

namespace Thunder {
namespace Exchange {
    // @json 1.0.0
    struct EXTERNAL IWiFi : virtual public Core::IUnknown {
        enum {
            ID = ID_WIFI
        };
        //...

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
// Generated automatically from 'IWiFi.h'. DO NOT EDIT.
#pragma once

#include "Module.h"
#include "JsonData_WiFi.h"
#include <interfaces/IWiFi.h>

namespace Thunder {

namespace Exchange {

    namespace JWiFi {

        namespace Version {

            constexpr uint8_t Major = 1;
            constexpr uint8_t Minor = 0;
            constexpr uint8_t Patch = 0;

        } // namespace Version

        using JSONRPC = PluginHost::JSONRPC;

        static void Register(JSONRPC& _module_, IWiFi* _impl_)
        {
            ASSERT(_impl_ != nullptr);

            _module_.RegisterVersion(_T("JWiFi"), Version::Major, Version::Minor, Version::Patch);

            // Register methods and properties...

            // Method: 'scan' - Start a WiFi scan
            _module_.Register<void, void>(_T("scan"), 
                [_impl_]() -> uint32_t {
                    uint32_t _errorCode;

                    _errorCode = _impl_->Scan();

                    return (_errorCode);
                });

            // Method: 'connect' - Connect to an access point
            _module_.Register<JsonData::WiFi::ConnectParamsData, void>(_T("connect"), 
                [_impl_](const JsonData::WiFi::ConnectParamsData& params) -> uint32_t {
                    uint32_t _errorCode;

                    const string _ssid{params.Ssid};

                    _errorCode = _impl_->Connect(_ssid);

                    return (_errorCode);
                });

            // Method: 'disconnect' - Disconnect from the currently connected access point
            _module_.Register<void, void>(_T("disconnect"), 
                [_impl_]() -> uint32_t {
                    uint32_t _errorCode;

                    _errorCode = _impl_->Disconnect();

                    return (_errorCode);
                });

        }

        static void Unregister(JSONRPC& _module_)
        {
            // Unregister methods and properties...
            _module_.Unregister(_T("scan"));
            _module_.Unregister(_T("connect"));
            _module_.Unregister(_T("disconnect"));
        }

        namespace Event {

            PUSH_WARNING(DISABLE_WARNING_UNUSED_FUNCTIONS)

            // Event: 'scancomplete' - Signal that a previously requested WiFi AP scan has completed
            static void ScanComplete(const JSONRPC& _module_, const JsonData::WiFi::ScanCompleteParamsData& params)
            {
                _module_.Notify(_T("scancomplete"), params);
            }

            // Event: 'scancomplete' - Signal that a previously requested WiFi AP scan has completed
            static void ScanComplete(const JSONRPC& _module_,
                     const Core::JSON::ArrayType<JsonData::WiFi::ScanCompleteParamsData::AccessPointData>& accessPoints)
            {
                JsonData::WiFi::ScanCompleteParamsData _params_;
                _params_.AccessPoints = accessPoints;

                ScanComplete(_module_, _params_);
            }

            // Event: 'scancomplete' - Signal that a previously requested WiFi AP scan has completed
            static void ScanComplete(const JSONRPC& _module_, const std::list<Exchange::IWiFi::AccessPoint>& accessPoints)
            {
                JsonData::WiFi::ScanCompleteParamsData _params_;
                _params_.AccessPoints = accessPoints;

                ScanComplete(_module_, _params_);
            }

            POP_WARNING()

        } // namespace Event

    } // namespace JWiFi

} // namespace Exchange

} // namespace Thunder

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
// C++ classes for WiFi API JSON-RPC API.
// Generated automatically from 'IWiFi.h'. DO NOT EDIT.

// Note: This code is inherently not thread safe. If required, proper synchronisation must be added.

#pragma once

#include <core/JSON.h>
#include <interfaces/IWiFi.h>

namespace Thunder {

namespace JsonData {

    namespace WiFi {

        // Method params/result classes
        //

        class ConnectParamsData : public Core::JSON::Container {
        public:
            ConnectParamsData()
                : Core::JSON::Container()
            {
                Add(_T("ssid"), &Ssid);
            }

            ConnectParamsData(const ConnectParamsData&) = delete;
            ConnectParamsData& operator=(const ConnectParamsData&) = delete;

        public:
            Core::JSON::String Ssid; //      SSID to connect to
        }; // class ConnectParamsData

        class ScanCompleteParamsData : public Core::JSON::Container {
        public:
            class AccessPointData : public Core::JSON::Container {
            public:
                AccessPointData()
                    : Core::JSON::Container()
                {
                    _Init();
                }

                AccessPointData(const AccessPointData& _other)
                    : Core::JSON::Container()
                    , Ssid(_other.Ssid)
                    , Channel(_other.Channel)
                    , Frequency(_other.Frequency)
                    , Signal(_other.Signal)
                {
                    _Init();
                }

                AccessPointData& operator=(const AccessPointData& _rhs)
                {
                    Ssid = _rhs.Ssid;
                    Channel = _rhs.Channel;
                    Frequency = _rhs.Frequency;
                    Signal = _rhs.Signal;
                    return (*this);
                }

                AccessPointData(const Exchange::IWiFi::AccessPoint& _other)
                    : Core::JSON::Container()
                {
                    Ssid = _other.ssid;
                    Channel = _other.channel;
                    Frequency = _other.frequency;
                    Signal = _other.signal;
                    _Init();
                }

                AccessPointData& operator=(const Exchange::IWiFi::AccessPoint& _rhs)
                {
                    Ssid = _rhs.ssid;
                    Channel = _rhs.channel;
                    Frequency = _rhs.frequency;
                    Signal = _rhs.signal;
                    return (*this);
                }

                operator Exchange::IWiFi::AccessPoint() const
                {
                    Exchange::IWiFi::AccessPoint _value{};
                    _value.ssid = Ssid;
                    _value.channel = Channel;
                    _value.frequency = Frequency;
                    _value.signal = Signal;
                    return (_value);
                }

            private:
                void _Init()
                {
                    Add(_T("ssid"), &Ssid);
                    Add(_T("channel"), &Channel);
                    Add(_T("frequency"), &Frequency);
                    Add(_T("signal"), &Signal);
                }

            public:
                Core::JSON::String Ssid;
                Core::JSON::DecUInt8 Channel;
                Core::JSON::DecUInt32 Frequency;
                Core::JSON::DecSInt32 Signal;
            }; // class AccessPointData

            ScanCompleteParamsData()
                : Core::JSON::Container()
            {
                Add(_T("accesspoints"), &AccessPoints);
            }

            ScanCompleteParamsData(const ScanCompleteParamsData&) = delete;
            ScanCompleteParamsData& operator=(const ScanCompleteParamsData&) = delete;

        public:
            Core::JSON::ArrayType<ScanCompleteParamsData::AccessPointData> AccessPoints;
        }; // class ScanCompleteParamsData

    } // namespace WiFi

} // namespace JsonData

}

Since there are no enums in this example interface, no JsonEnums_WiFi.cpp file was generated.