The Starboard Extension framework provides a way to add optional, platform-specific features to the Starboard application. A Starboard Extension is an optional interface that porters can implement for their platforms if, and as, they wish.
This tutorial uses coding exercises to guide you through the process of creating a simple example of a Starboard Extension. By the end you should have a firm understanding of what Starboard Extensions are, when to use them instead of alternatives, how to write them, and how to work with the Cobalt team to contribute them to the repository.
Prerequisites
Because it's helpful to build and run Cobalt during the exercises, you'll first want to set up your environment and make sure you can build Cobalt. You can follow Set up your environment - Linux to do this if you're a Linux user. Please note that the exercise solutions assume you're using Linux but should be comparable to implementations for other platforms.
Also note that while this codelab doesn't require it, you'll need to Port Cobalt to your platform before you can actually use a Starboard Extension to customize it for your platform.
Finally, the exercises assume the ability to program in C and C++.
Exercise 0: Run Cobalt and inspect logs
Assuming you've already built Cobalt, please now run Cobalt and pay special attention to a message it logs when it starts up. This message will be the focus of subsequent exercises.
$ out/linux-x64x11_debug/cobalt 2>/dev/null | grep "Starting application"
Background
Situated below Cobalt is Starboard. Starboard, which is a porting layer and OS abstraction, contains a minimal set of APIs to encapsulate the platform-specific functionalities that Cobalt uses. Each Starboard module (memory, socket, thread, etc.) defines functions that must be implemented on a porter's platform, imposing an implementation and maintenance cost on all porters. With this cost in mind the Cobalt team tries to keep the Starboard APIs stable and only adds a new API when some functionality is required by Cobalt but the implementation depends on the platform.
A Starboard API can be made optional, though, by the introduction of an
accompanying API that asks platforms whether they support the underlying feature
and enables Cobalt to check for the answer at runtime. For example,
SbWindowOnScreenKeyboardIsSupported
is used so that only platforms that
support an on screen keyboard need implement the related functions in
starboard/window.h
. To spare porters uninterested in the functionality, the
Cobalt team chooses to make a Starboard API optional when some Cobalt
functionality is optional and the implementation is platform-dependent.
Finally, a nonobvious point explains why even an optional Starboard API may sometimes be too cumbersome: other applications beyond Cobalt are able to be run on top of Starboard. If a feature is needed by Cobalt but not by all Starboard- based applications or by Starboard itself, adding a Starboard API for it may add unnecessary size and complexity to the porting layer. And here we arrive at the sweet spot for Starboard Extensions: when the desired functionality is Cobalt-specific, optional in Cobalt, and has platform-dependent implementation. Also, because Starboard Extensions are lightweight and, as you'll see below, added without any changes to the Starboard layer, they're the preferred way for porters to add new, custom features to Cobalt.
To summarize:
Tool | Use case | Ecosystem cost |
---|---|---|
Starboard API | Feature is required but implementation is platform-dependent | High |
Optional Starboard API | Feature is optional and implementation is platform-dependent | Medium |
Starboard Extension | Feature is optional and specific to Cobalt and implementation is platform-dependent | Low |
As a caveat, please note that for all three of these abstractions the interface is in Cobalt's open-source repository and therefore visible to other porters. The implementation, on the other hand, is written and built by porters and so may be kept private.
Finally, in addition to the alternatives mentioned, porters have in some cases made local changes to Cobalt, above the Starboard layer, to achieve some customization or optimization. This has been discouraged by the Cobalt team because it makes rebasing to future versions of Cobalt more difficult but has been possible because porters have historically built both Cobalt and Starboard. However, Cobalt is moving toward Evergreen (overview), an architecture that enables automatic Cobalt updates on devices by separating a Google-built, Cobalt core shared library from the partner-built Starboard layer and Cobalt loader app. Because Cobalt core code is built by Google, custom changes to it are no longer possible for partners using Evergreen.
Anatomy of a Starboard Extension
Extension structures
Cobalt uses a structure to describe the interface for an extension and organizes
the structures in headers under starboard/extension/
. The header for a "foo"
extension should be named foo.h
and the first version of it should contain the
following content, as well as any additional members that provide the "foo"
functionality.
#ifndef STARBOARD_EXTENSION_FOO_H_
#define STARBOARD_EXTENSION_FOO_H_
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define kStarboardExtensionFooName "dev.starboard.extension.Foo"
typedef struct StarboardExtensionFooApi {
// Name should be the string |kStarboardExtensionFooName|.
// This helps to validate that the extension API is correct.
const char* name;
// This specifies the version of the API that is implemented.
uint32_t version;
// The fields below this point were added in version 1 or later.
} StarboardExtensionFooApi;
#ifdef __cplusplus
} // extern "C"
#endif
#endif // STARBOARD_EXTENSION_FOO_H_
Please note a few important points about this structure:
- The first two members must be, in order:
- A
const char* |name|
, storing the extension's name. - A
uint32_t |version|
, storing the version number of the extension. Extension versioning is discussed later on in this codelab.
- A
- The following members can be any C types (including custom structures) that are useful. Often, these are function pointers.
Extension access in Cobalt
The SbSystemGetExtension
function from Starboard's system
module allows
Cobalt to query for an extension by name. It returns a pointer to the constant,
global instance of the structure implementing the extension with the given name
if it exists, otherwise NULL
. This function is the only mechanism Cobalt has
to get an extension; the Starboard interface intentionally doesn't have any
functions related to the specific extensions.
The caller in Cobalt must static cast the const void*
returned by
SbSystemGetExtension
to a const StarboardExtensionFooApi*
, or pointer of
whatever type the extension structure type happens to be, before using it. Since
the caller can't be sure whether a platform implements the extension or, if it
does, implements it correctly, it's good defensive programming to check that the
resulting pointer is not NULL
and that the name
member in the pointed-to
structure has the same value as kStarboardExtensionFooName
.
Extension implementation
Because Starboard Extensions are platform-dependent, the implementations of an
extension belong in Starboard ports. A Starboard port implements an extension by
defining a constant, global instance of the structure and implementing the
SbSystemGetExtension
function to return a pointer to it. For our "foo"
extension, an implementation for custom_platform
's Starboard port could look
as follows.
starboard/custom_platform/foo.h
declares a GetFooApi
accessor for the
structure instance.
#ifndef STARBOARD_CUSTOM_PLATFORM_FOO_H_
#define STARBOARD_CUSTOM_PLATFORM_FOO_H_
namespace starboard {
namespace custom_platform {
const void* GetFooApi();
} // namespace custom_platform
} // namespace starboard
#endif // STARBOARD_CUSTOM_PLATFORM_FOO_H_
starboard/custom_platform/foo.cc
, then, defines GetFooApi
.
#include "starboard/custom_platform/foo.h"
#include "starboard/extension/foo.h"
namespace starboard {
namespace custom_platform {
namespace {
// Definitions of any functions included as components in the extension
// are added here.
const StarboardExtensionFooApi kFooApi = {
kStarboardExtensionFooName,
1, // API version that's implemented.
// Any additional members are initialized here.
};
} // namespace
const void* GetFooApi() {
return &kFooApi;
}
} // namespace custom_platform
} // namespace starboard
Finally, starboard/custom_platform/system_get_extension.cc
defines
SbSystemGetExtension
to wire up the extensions for the platform.
#include "starboard/system.h"
#include "starboard/extension/foo.h"
#include "starboard/common/string.h"
#include "starboard/custom_platform/foo.h"
const void* SbSystemGetExtension(const char* name) {
if (strcmp(name, kStarboardExtensionFooName) == 0) {
return starboard::custom_platform::GetFooApi();
}
// Other conditions here should handle other implemented extensions.
return NULL;
}
Please feel free to browse existing extension implementations in the repository.
For example, the reference Raspberry Pi port implements the Graphics
extension
across the following files.
-
starboard/raspi/shared/graphics.h
-
starboard/raspi/shared/graphics.cc
-
starboard/raspi/shared/system_get_extensions.cc
Exercise 1: Write and use your first extension
Now that you've seen the anatomy of a Starboard Extension it's your turn to write
one of your own. In Exercise 0 we saw that Cobalt logs "Starting application"
when it's started. Please write a Pleasantry
Starboard Extension that has a
member of type const char*
and name greeting
and make any necessary changes
in cobalt/browser/main.cc
so that the extension can be used to log a custom
greeting directly after the plain "Starting application." Implement the
extension for Linux, or whichever other platform you'd like, and confirm that
the greeting is logged.
Solution to Exercise 1
Click the items below to expand parts of a solution. The git diff
s are between
the solution and the master
branch.
Contents of new
starboard/extension/pleasantry.h
file.
#ifndef STARBOARD_EXTENSION_PLEASANTRY_H_
#define STARBOARD_EXTENSION_PLEASANTRY_H_
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define kStarboardExtensionPleasantryName "dev.starboard.extension.Pleasantry"
typedef struct StarboardExtensionPleasantryApi {
// Name should be the string |kStarboardExtensionPleasantryName|.
// This helps to validate that the extension API is correct.
const char* name;
// This specifies the version of the API that is implemented.
uint32_t version;
// The fields below this point were added in version 1 or later.
const char* greeting;
} StarboardExtensionPleasantryApi;
#ifdef __cplusplus
} // extern "C"
#endif
#endif // STARBOARD_EXTENSION_PLEASANTRY_H_
Contents of new
starboard/linux/shared/pleasantry.h
file.
#ifndef STARBOARD_LINUX_SHARED_PLEASANTRY_H_
#define STARBOARD_LINUX_SHARED_PLEASANTRY_H_
namespace starboard {
namespace shared {
const void* GetPleasantryApi();
} // namespace shared
} // namespace starboard
#endif // STARBOARD_LINUX_SHARED_PLEASANTRY_H_
Contents of new
starboard/linux/shared/pleasantry.cc
file.
#include "starboard/linux/shared/pleasantry.h"
#include "starboard/extension/pleasantry.h"
namespace starboard {
namespace shared {
namespace {
const char *kGreeting = "Happy debugging!";
const StarboardExtensionPleasantryApi kPleasantryApi = {
kStarboardExtensionPleasantryName,
1,
kGreeting,
};
} // namespace
const void* GetPleasantryApi() {
return &kPleasantryApi;
}
} // namespace shared
} // namespace starboard
git diff
starboard/linux/shared/BUILD.gn
@@ -71,6 +71,8 @@ static_library("starboard_platform_sources") {
"//starboard/linux/shared/netlink.cc",
"//starboard/linux/shared/netlink.h",
"//starboard/linux/shared/player_components_factory.cc",
+ "//starboard/linux/shared/pleasantry.cc",
+ "//starboard/linux/shared/pleasantry.h",
"//starboard/linux/shared/routes.cc",
"//starboard/linux/shared/routes.h",
"//starboard/linux/shared/soft_mic_platform_service.cc",
git diff
starboard/linux/shared/system_get_extensions.cc
@@ -22,6 +22,7 @@
#include "starboard/extension/free_space.h"
#include "starboard/extension/memory_mapped_file.h"
#include "starboard/extension/platform_service.h"
+#include "starboard/extension/pleasantry.h"
#include "starboard/linux/shared/soft_mic_platform_service.h"
#include "starboard/shared/enhanced_audio/enhanced_audio.h"
#include "starboard/shared/ffmpeg/ffmpeg_demuxer.h"
@@ -33,6 +34,7 @@
#include "starboard/elf_loader/evergreen_config.h"
#endif
#include "starboard/linux/shared/configuration.h"
+#include "starboard/linux/shared/pleasantry.h"
const void* SbSystemGetExtension(const char* name) {
#if SB_IS(EVERGREEN_COMPATIBLE)
@@ -74,5 +76,8 @@ const void* SbSystemGetExtension(const char* name) {
return use_ffmpeg_demuxer ? starboard::shared::ffmpeg::GetFFmpegDemuxerApi()
: NULL;
}
+ if (strcmp(name, kStarboardExtensionPleasantryName) == 0) {
+ return starboard::shared::GetPleasantryApi();
+ }
return NULL;
}
git diff cobalt/browser/main.cc
@@ -19,6 +19,8 @@
#include "cobalt/browser/application.h"
#include "cobalt/browser/switches.h"
#include "cobalt/version.h"
+#include "starboard/extension/pleasantry.h"
+#include "starboard/system.h"
namespace {
@@ -77,6 +79,14 @@ void StartApplication(int argc, char** argv, const char* link,
return;
}
LOG(INFO) << "Starting application.";
+ const StarboardExtensionPleasantryApi* pleasantry_extension =
+ static_cast<const StarboardExtensionPleasantryApi*>(
+ SbSystemGetExtension(kStarboardExtensionPleasantryName));
+ if (pleasantry_extension &&
+ strcmp(pleasantry_extension->name, kStarboardExtensionPleasantryName) == 0 &&
+ pleasantry_extension->version >= 1) {
+ LOG(INFO) << pleasantry_extension->greeting;
+ }
#if SB_API_VERSION >= 13
DCHECK(!g_application);
g_application = new cobalt::browser::Application(quit_closure,
Extension versioning
Starboard Extensions are themselves extensible, but care must be taken to ensure that the extension interface in Cobalt and implementation in a platform's port, which may be built separately, are consistent.
The version
member, which is always the second member in an extension
structure, indicates which version of the interface the structure describes. In
other words, a version
of the extension structure corresponds to a specific,
invariant list of members. By convention, the first version of a Cobalt
Extension is version 1
(i.e., one-based indexing, not zero-based).
A new version of the extension can be introduced in the structure declaration by adding additional members to the end of the declaration and adding a comment to delineate, e.g., "The fields below this point were added in version 2 or later." To maintain compatibility and enable Cobalt to correctly index into instances of the structure, it's important that members are always added at the end of the structure declaration and that members are never removed. If a member is deprecated in a later version, this fact should simply be noted with a comment in the structure declaration.
To implement a new version of the extension, a platform's port should then set
the version
member to the appropriate value when creating the instance of the
structure, and also initialize all members required for the version.
Finally, any code in Cobalt that uses the extension should guard references to members with version checks.
Exercise 2: Version your extension
Add a second version of the Pleasantry
extension that enables porters to also
log a polite farewell message when the Cobalt application is stopped. To allow
platforms more flexibility, add the new farewell
member as a pointer to a
function that takes no parameters and returns a const char*
. Update
cobalt/browser/main.cc
so that Cobalt, if the platform implements version 2 of
this extension, replaces the "Stopping application." message with a polite
farewell provided by the platform.
To keep things interesting, have the platform's implementation pseudo-randomly return one of several messages. And, once you've made the changes, build Cobalt and run it a few times to confirm that the feature behaves as expected.
Solution to Exercise 2
Click the items below to expand parts of a solution. The git diff
is between
the solution and the master
branch.
Updated contents of
starboard/extension/pleasantry.h
.
#ifndef STARBOARD_EXTENSION_PLEASANTRY_H_
#define STARBOARD_EXTENSION_PLEASANTRY_H_
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define kStarboardExtensionPleasantryName "dev.starboard.extension.Pleasantry"
typedef struct StarboardExtensionPleasantryApi {
// Name should be the string |kStarboardExtensionPleasantryName|.
// This helps to validate that the extension API is correct.
const char* name;
// This specifies the version of the API that is implemented.
uint32_t version;
// The fields below this point were added in version 1 or later.
const char* greeting;
// The fields below this point were added in version 2 or later.
const char* (*GetFarewell)();
} StarboardExtensionPleasantryApi;
#ifdef __cplusplus
} // extern "C"
#endif
#endif // STARBOARD_EXTENSION_PLEASANTRY_H_
Updated contents of
starboard/linux/shared/pleasantry.cc
.
#include "starboard/linux/shared/pleasantry.h"
#include <stdlib.h>
#include "starboard/extension/pleasantry.h"
#include "starboard/system.h"
#include "starboard/time.h"
namespace starboard {
namespace shared {
namespace {
const char* kGreeting = "Happy debugging!";
const char* kFarewells[] = {
"Farewell",
"Take care",
"Thanks for running Cobalt",
};
const char* GetFarewell() {
srand (SbTimeGetNow());
int pseudo_random_index = rand() % SB_ARRAY_SIZE_INT(kFarewells);
return kFarewells[pseudo_random_index];
}
const StarboardExtensionPleasantryApi kPleasantryApi = {
kStarboardExtensionPleasantryName,
2,
kGreeting,
&GetFarewell,
};
} // namespace
const void* GetPleasantryApi() {
return &kPleasantryApi;
}
} // namespace shared
} // namespace starboard
git diff cobalt/browser/main.cc
@@ -19,6 +19,8 @@
#include "cobalt/browser/application.h"
#include "cobalt/browser/switches.h"
#include "cobalt/version.h"
+#include "starboard/extension/pleasantry.h"
+#include "starboard/system.h"
namespace {
@@ -54,6 +56,14 @@ bool CheckForAndExecuteStartupSwitches() {
return g_is_startup_switch_set;
}
+// Get the Pleasantry extension if it's implemented.
+const StarboardExtensionPleasantryApi* GetPleasantryApi() {
+ static const StarboardExtensionPleasantryApi* pleasantry_extension =
+ static_cast<const StarboardExtensionPleasantryApi*>(
+ SbSystemGetExtension(kStarboardExtensionPleasantryName));
+ return pleasantry_extension;
+}
+
void PreloadApplication(int argc, char** argv, const char* link,
const base::Closure& quit_closure,
SbTimeMonotonic timestamp) {
@@ -77,6 +87,14 @@ void StartApplication(int argc, char** argv, const char* link,
return;
}
LOG(INFO) << "Starting application.";
+ const StarboardExtensionPleasantryApi* pleasantry_extension =
+ static_cast<const StarboardExtensionPleasantryApi*>(
+ SbSystemGetExtension(kStarboardExtensionPleasantryName));
+ if (pleasantry_extension &&
+ strcmp(pleasantry_extension->name, kStarboardExtensionPleasantryName) == 0 &&
+ pleasantry_extension->version >= 1) {
+ LOG(INFO) << pleasantry_extension->greeting;
+ }
#if SB_API_VERSION >= 13
DCHECK(!g_application);
g_application = new cobalt::browser::Application(quit_closure,
@@ -96,7 +114,14 @@ void StartApplication(int argc, char** argv, const char* link,
}
void StopApplication() {
- LOG(INFO) << "Stopping application.";
+ const StarboardExtensionPleasantryApi* pleasantry_extension = GetPleasantryApi();
+ if (pleasantry_extension &&
+ strcmp(pleasantry_extension->name, kStarboardExtensionPleasantryName) == 0 &&
+ pleasantry_extension->version >= 2) {
+ LOG(INFO) << pleasantry_extension->GetFarewell();
+ } else {
+ LOG(INFO) << "Stopping application.";
+ }
delete g_application;
g_application = NULL;
}
starboard/linux/shared/pleasantry.h
,
starboard/linux/shared/BUILD.gn
, and
starboard/linux/shared/system_get_extensions.cc
should be unchanged from the
Exercise 1 solution.
Extension testing
Each Starboard Extension has a test in starboard/extension/extension_test.cc
that
tests whether the extension is wired up correctly for the platform Cobalt
happens to be built for.
Since some platforms may not implement a particular extension, these tests begin
by checking whether SbSystemGetExtension
simply returns NULL
for the
extension's name. For our foo
extension, the first few lines may contain the
following.
TEST(ExtensionTest, Foo) {
typedef StarboardExtensionFooApi ExtensionApi;
const char* kExtensionName = kStarboardExtensionFooName;
const ExtensionApi* extension_api =
static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
if (!extension_api) {
return;
}
// Verifications about the global structure instance, if implemented.
}
If SbSystemGetExtension
does not return NULL
, meaning the platform does
implement the extension, the tests generally verify a few details about the
structure:
- It has the expected name.
- Its version is in the range of possible versions for the extension.
- For whichever version is implemented, any members required for that version are present.
- It's a singleton.
Exercise 3: Test your extension
You guessed it! Add a test for your new extension to
starboard/extension/extension_test.cc
.
Once you've written your test you can execute it to confirm that it passes.
starboard/extension/BUILD.gn
configures an extension_test
target to be built
from our extension_test.cc
source file. We can build that target for our
platform and then run the executable to run the tests.
$ cobalt/build/gn.py -p linux-x64x11
$ ninja -C out/linux-x64x11_devel all
$ out/linux-x64x11_devel/extension_test
Tip: because the extension_test
has type <(gtest_target_type)
, we can use
--gtest_filter
to filter the tests that are run. For example, you can run just
your newly added test with --gtest_filter=ExtensionTest.Pleasantry
.
Solution to Exercise 3
Click here to see a solution for the new
test.
TEST(ExtensionTest, Pleasantry) {
typedef StarboardExtensionPleasantryApi ExtensionApi;
const char* kExtensionName = kStarboardExtensionPleasantryName;
const ExtensionApi* extension_api =
static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
if (!extension_api) {
return;
}
EXPECT_STREQ(extension_api->name, kExtensionName);
EXPECT_GE(extension_api->version, 1u);
EXPECT_LE(extension_api->version, 2u);
EXPECT_NE(extension_api->greeting, nullptr);
if (extension_api->version >= 2) {
EXPECT_NE(extension_api->GetFarewell, nullptr);
}
const ExtensionApi* second_extension_api =
static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
EXPECT_EQ(second_extension_api, extension_api)
<< "Extension struct should be a singleton";
}
You'll also want to include the header for the extension, i.e., #include
"starboard/extension/pleasantry.h"
.
Contributing a Starboard Extension
Thanks for taking the time to complete the codelab!
If you'd like to contribute an actual Starboard Extension to Cobalt in order to add some useful functionality for your platform, we encourage you to start a discussion with the Cobalt team before you begin coding. To do so, please file a feature request for the extension using this template.
Please file this feature request with the appropriate priority and the Cobalt team will review the proposal accordingly. If the Cobalt team approves of the use case and design then a member of the team will assign the feature request back to you for implementation. At this point, please follow the Contributing to Cobalt guide to ensure your code is compliant and can be reviewed and submitted.