You.i Engine One: Structuring Native Code for Multi-Platform Support

John Cassidy
8 min readJan 7, 2020

A major value gained by using You.i Engine One is creating a single codebase to service all supported platforms. The reason this is possible is because You.i Engine One is a rendering engine, written in C++, that operates cross-platform.

Some of the platforms supported by You.i Engine One

Platform specific code that does exist is presented through abstractions. These abstractions allow the code that you write to remain platform agnostic. With the introduction of React Native to the You.i Engine One stack, developers can also write JSX code treating the engine as a single platform.

This article will broach the subject of writing platform specific code for those situations where you have no choice: Native Modules that require specific platform SDK integrations.

I will present a structure that I like to follow (and mirrors the approach taken in the Engine itself) to provide the appropriate abstraction to allow your JSX code to remain platform agnostic while separating out your platform specific code to allow for maintainability and readability, easier platform expansion for future work, and providing default behaviour to allow you to continue to build for all supported platforms.

Writing Maintainable and Extensible Native Code

When writing native C++ code for a Native Module, the challenge is to structure your code for re-use across platforms, but to also differentiate and isolate logic when necessary.

If you are writing a Native Module that requires the platform SDK to have certain features, such as the ability to monitor a change in System Theme, there are going to be situations where:

  • The platform SDK is written in a language that differs from other platforms (Objective-C or Swift for iOS and tvOS and Java for Android), or
  • The platform SDK does not support the functionality, or
  • The platform SDK does support the functionality, but you are not going to implement it until a later date.

All of these scenarios can lead to some ugly code patterns.

Separating Platform Specific Functionality

Typically, you want your JSX code to interact with your Native Module in a similar way regardless of platform. For the example of a Theme Detection Module, you want to be able to subscribe to changes in the system theme as well as request the current theme value at that moment.

Common header file for Dark Mode Native Module that will be used across all platforms

With the above header implementation, you will see that there is no platform specific includes or private members used that may cause confusion or headache when attempting to maintain this code. In its most simple form it provides the core functionality and purpose of the Native Module, regardless of Platform.

There is a class member pointer to a DarkModeModulePriv type, which is forward declared. What this means is that the compiler does not need to know any information about the class apart from the fact that it exists and there will be an implementation for it when needed.

Generic pattern for native modules that allows for expanding to new platforms

Since it is a pointer, it will always be 4 or 8 bytes (32bit vs 64bit). If this was a reference to a class, it would need to allocate the appropriate memory and need to know a lot more information about the structure.

Continuing with the example of a Theme Detection Module (DarkModeModule), with this simplified pattern, each platform is going have their own definition of DarkModeModulePriv and implementation of both DarkModeModule and DarkModeModulePriv . For platforms that are not supported, a default definition and implementation will be used to provide default or stubbed out behaviour.

This allows for iOS and tvOS code to contain Objective-C or Swift code to be compiled, and also allows for Android to have an implementation that contains a heavy load of JNI definitions to interact with Java.

Closing the loop

There is still something missing before we can use the above structure to write something useful: A clean way for the DarkModeModulePriv class to communicate asynchronously back to DarkModeModule which can then emit to JS (remember, DarkModeModule is what is registered as the Event Emitter, only it can emit an event that is listened to by JSX).

This is solved by marking the DarkModeModulePriv instance as a friend class of DarkModeModule. You can then define private or protected callback methods, and pass an instance of DarkModeModule to the DarkModeModulePriv implementation to allow information to flow from DarkModeModule to DarkModeModulePriv to DarkModeModule again. (this does not explicitly require a friend class, you can always just make your callbacks public).

Sequence Diagram showing flow from JSX to platform to JSX again

With the above applied in a generic sense, a Native Module definition can be presented in an abstract way that JSX code interacts with it in an identical manner regardless of platform. The platform specific implementations (concerns) are kept separate from one another, preventing a (in my opinion) hard to maintain mess.

Listening to Theme Changes on multiple platforms

Dark Mode All The Things — Photo by Scott Stefan on Unsplash

A typical DarkModeModule implementation would look like the following, where interaction with JSX occurs but most business logic requests are passed to its private platform specific implementation:

The DarkModeModulePriv would then be constructed and defined to accept the DarkModeModule as an argument to allow it to access any callback methods:

Basic example of platform specific DarkModeModulePriv

Choosing Platform Specific Implementations

In order for the build system to know which implementations should be used on a per platform basis, we need to specify some platform specific files to be included as source in our SourceList.cmake.

We can make use of the cmake flags IOS, ANDROID, and OSX for the platforms we have specific implementations for, and allow for the other platforms to fall to default implementations

This will ensure there are no collisions. An alternative approach is to pull in all these files, and then inline the pre-compile flags YI_ANDROID, YI_IOS and YI_OSX to guard against them being compiled. I personally feel this is a bit more prone to error, but if you prefer to keep things app-side then it is a possibility.

Your folder structure that contains all possible implementations, may look as follows:

Sample structure that shows multiple platform implementations

macOS platform specific implementation

Listening to Theme Changes in macOS

For a macOS implementation, the first path is to setup a listener in your private implementation

This is platform specific macOS code that exists to receive events from the SDK, and pass them to a listening bridge. It can be invoked in the private implementation of DarkModeModulePriv.

The callback DarkModeModule::OnModeChange is invoked, and the event is emitted to any JS listeners.

iOS platform specific implementation

This sample borrows from react-native-dark-mode iOS implementation to listen to changes. Existing packages that have native implementations can easily be used, it may just involve bringing the code app-side (and marking appropriately as per any license involved), and interacting with it via your DarkModeModulePriv implementation.

The platform specific implementation that our implementation can pull in specifically is located here on github.

We can pull it app-side and then reference it with our implementation in a straightforward manner.

Once again, the callback DarkModeModule::OnModeChange is invoked, and the event is emitted to any JS listeners.

Android Platform Specific Implementation

Listening to theme changes on Android 10

Android is similar to iOS and macOS in that the platform specific implementation is completely unique for the platform. Specifically, to interact with the SDK directly you typically need access to the activity context (available via your main Activity) and this can be done via java. (by default, your main activity is CYIActivity.java that is bundled with the engine)

In order to integrate with android Java, you will need to communicate via JNI.

As such, some platform specific includes in the implementation will be tools used in order to communicate with java. [A more detailed post on C++ and Android communication via JNI will come in a separate post].

The above provides externed pointers to both the JVM that will be used to interact with JNI, and our main Activity that we want to provide to java in order to retrieve contextual information that is key for many SDK calls. We can then define a DarkModeModulePriv header that is aware of JNI.

The implementation will look a bit heavy handed, but its purpose is to discover java classes, instantiate them, and store references to methods that can be invoked at a later time.

We can then implement our platform specific code (in java) to interact with Android SDK and callback to our native code.

The above can be a bit confusing with the inclusion of JNI and java, but it’s really a matter of scoping and encapsulation (who is allowed to access what and where are callbacks occurring):

  1. DarkModeModule receives Intent to subscribe to Theme Changes from JSX
  2. DarkModeModulePriv implementation is instantiated with the ability to callback to DarkModeModule
  3. DarkModeModulePriv sets up the JNI bridge to communicate to Java. Method calls into java are synchronous.
  4. Java class is instantiated and starts listening to platform changes in theme
  5. On theme change, a native JNI call is made back to C++. This is _not_ within the scope of DarkModeModulePriv, but a pointer is provided that we are allowed to cast to DarkModeModulePriv. We can then invoke a public callback method on it.
  6. Within that public callback method, we can invoke our friend class DarkModeModule private callback
  7. From DarkModeModule we emit the signal to JSX that the theme change has occurred.

Default Platform Specific Integration

For platforms that do not support theme changes, we simply want a stubbed out implementation that returns a default theme and does not emit any events.

Concerns are Separated

For this use case, we have three unique implementations for iOS, Android, and macOS. We have a fallback default that performs no service other than providing a default value of our choosing.

With all the above unique implementations, when building on each platform, none of the code is aware of other platform specific code. The abstraction is placed at the correct location to allow the JSX to be written in a platform agnostic fashion.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

No responses yet

Write a response