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

John Cassidy
John Cassidy

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.

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)
  • The platform SDK does not support the functionality
  • 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.

#ifndef _DARK_MODE_MODULE_H_
#define _DARK_MODE_MODULE_H_

#include <youireact/NativeModule.h>
#include <youireact/modules/EventEmitter.h>

class DarkModeModulePriv;

/**
This class represents a Native Module that is accessible via JSX. It extends the EventEmitterModule
meaning that it can emit events that can be subscribed to by JSX.
 */
class YI_RN_MODULE(DarkModeModule, yi::react::EventEmitterModule)
{
public:
    // Marking DarkModeModulePriv as a friend class. This means that DarkModeModulePriv implementation
    // will have access to this classes private members and methods. In particular, it will have access
    // to the callback that will be used to then notify our JSX via the EventEmitterModule.
    friend class DarkModeModulePriv;
    
    DarkModeModule();
    virtual ~DarkModeModule() final;
  
    // Export name for module that will be referenced in JSX
    YI_RN_EXPORT_NAME(DarkModeModule);

    // a constant available on the native module accessible from JSX
    YI_RN_EXPORT_CONSTANT(currentMode);

private:
    
    // Callback function to indicate that the system theme (mode) has changed
    void OnModeChange(CYIString mode);
    
    // private member of our private implementation (that will be platform specific)
    // this will be forward declared, meaning that the compiler simply needs to know
    // that this is a pointer to _something_ that will be linked correctly.
    // each platform will implement a variation of this class.
    DarkModeModulePriv *m_pPriv;
};

#endif // _DARK_MODE_MODULE_H_

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 are 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 to 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:

YI_RN_INSTANTIATE_MODULE(DarkModeModule, yi::react::EventEmitterModule);
YI_RN_REGISTER_MODULE(DarkModeModule);

static const std::string MODE_CHANGE = "MODE_CHANGE";

DarkModeModule::DarkModeModule() : m_pPriv(new DarkModeModulePriv(this))
{
    SetSupportedEvents
    ({
       MODE_CHANGE
    });
}

DarkModeModule::~DarkModeModule() = default;

YI_RN_DEFINE_EXPORT_CONSTANT(DarkModeModule, currentMode)
{
    return folly::dynamic(m_pPriv->GetCurrent());
}

void DarkModeModule::OnModeChange(CYIString mode)
{
    EmitEvent(MODE_CHANGE, { mode });
}

 

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

#ifndef _DARK_MODE_MODULE_PRIV_H_
#define _DARK_MODE_MODULE_PRIV_H_

class DarkModeModule;

class DarkModeModulePriv
{
public:
    DarkModeModulePriv(DarkModeModule *pPub);
    virtual ~DarkModeModulePriv() final;
  
    CYIString GetCurrent();
    
    void NotifyChange();

private:
    DarkModeModule *m_pPub;
};

#endif // _DARK_MODE_MODULE_PRIV_H_

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 a 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.

# =============================================================================
# © You i Labs Inc. 2000-2019. All rights reserved.

# In this file you can control whatever files are pulled in as source and headers
# as long as you provide them to the variables YI_PROJECT_SOURCE and YI_PROJECT_HEADERS
# respectively. 

# You can also specify platform specific files to pull in, this can be used to not
# use pre-compilation flags within your code, but instead to completely omit a file 
# similar to a blacklist for platforms that you dont' care about
if(IOS)

  # Declare the source and header files for ios module implementations
  set(YI_PROJECT_MODULES_SOURCE
    src/modules/ios/DarkModeModule_iOS.mm
    src/modules/ios/UIScreen+RNDarkModeTraitChangeListener.mm
  )
  set (YI_PROJECT_MODULES_HEADERS
    src/modules/ios/DarkModeModulePriv_iOS.h
    src/modules/ios/RNDarkMode.h
    src/modules/ios/UIScreen+RNDarkModeTraitChangeListener.h
  )

elseif(ANDROID)

#   # Declare the source and header files for android module implementations
  set(YI_PROJECT_MODULES_SOURCE
    src/modules/android/DarkModeModule_Android.cpp
  )
  set (YI_PROJECT_MODULES_HEADERS
    src/AndroidCommon.h
    src/modules/android/DarkModeModulePriv_Android.h
  )

  elseif(OSX)

  # Declare the source and header files for osx module implementations
  set(YI_PROJECT_MODULES_SOURCE
    src/modules/osx/DarkModeModule_osx.mm
  )
  set (YI_PROJECT_MODULES_HEADERS
    src/AndroidCommon.h
    src/modules/osx/DarkModeModulePriv_osx.h
  )

else()

  # Declare the source and header files for default module implementations
  set(YI_PROJECT_MODULES_SOURCE
    src/modules/DarkModeModule_Default.cpp
  )
  set (YI_PROJECT_MODULES_HEADERS
    src/modules/DarkModeModulePriv_Default.h
  )

endif()

# set all project source files, including platform specific ones above
set (YI_PROJECT_SOURCE
    src/App.cpp
    src/AppFactory.cpp
    ${YI_PROJECT_MODULES_SOURCE}
)

# set all project header files, including platform specific ones above
set (YI_PROJECT_HEADERS
    src/App.h
    src/modules/DarkModeModule.h
    ${YI_PROJECT_MODULES_HEADERS}
)

 

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 set up a listener in your private implementation.

#import <AppKit/AppKit.h>

@interface ThemeObserver : NSObject
+ (ThemeObserver *)sharedInstance;
- (void)startThemeObserver;
- (void)setBridge:(DarkModeModulePriv *)pBridge;

@end

@implementation ThemeObserver

DarkModeModulePriv *m_pBridge;

static ThemeObserver *theObserver = nil;
static std::mutex theObjserverMutex;

+ (ThemeObserver *)sharedInstance {
    if (theObserver == nil)
    {
        std::unique_lock<std::mutex> lock(theObjserverMutex);
        if (theObserver == nil)
        {
            id obj = [[self alloc] init];
            if (obj)
            {
                theObserver = obj;
            }
        }
    }
    return theObserver;
}


- (void)startThemeObserver {
    [NSDistributedNotificationCenter.defaultCenter addObserver:self selector:@selector(themeChanged:) name:@"AppleInterfaceThemeChangedNotification" object: nil];
}

- (void)setBridge:(DarkModeModulePriv *)pBridge {
    m_pBridge = pBridge;
}

-(void)themeChanged:(NSNotification *) notification {
    m_pBridge->NotifyChange();
}

@end

 

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.

DarkModeModulePriv::DarkModeModulePriv(DarkModeModule *pPub)
: m_pPub(pPub)
{
    // set callback bridge and listen to theme changes from our observer
    [[ThemeObserver sharedInstance] setBridge:this];
    [[ThemeObserver sharedInstance] startThemeObserver];
}


DarkModeModulePriv::~DarkModeModulePriv() = default;

CYIString DarkModeModulePriv::GetCurrent()
{
    // check to determine if interface style is stored in user defaults
    NSString *value = (NSString *)[[NSUserDefaults standardUserDefaults] objectForKey:@"AppleInterfaceStyle"];
    CYIString sValue = CYIString(value);
    
    // if no value returns from our user defaults, retrieve theme that app initially launched with
    if (sValue.IsEmpty()) {
        NSAppearance *pAppearance = NSAppearance.currentAppearance;
        if (pAppearance.name == NSAppearanceNameDarkAqua) {
            sValue = "Dark";
        }
    }
    
    if (sValue.Compare("Dark") == 0) {
        return CYIString("dark");
    }
    
    return CYIString("light");
}

void DarkModeModulePriv::NotifyChange()
{
    // notify our friend class callback of the value stored in the user defaults as noted by the change coming from our observer
    NSString *value = (NSString *)[[NSUserDefaults standardUserDefaults] objectForKey:@"AppleInterfaceStyle"];
    CYIString sValue = CYIString(value);
    if (sValue.Compare("Dark") == 0) {
        m_pPub->OnModeChange("dark");
    } else {
        m_pPub->OnModeChange("light");
    }
}

 

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.

DarkModeModulePriv::DarkModeModulePriv(DarkModeModule *pPub) : m_pPub(pPub)
{
    // start listening for changes to dark mode traits
    [UIScreen setCurrentManager:this];
}

DarkModeModulePriv::~DarkModeModulePriv() = default;

CYIString DarkModeModulePriv::GetCurrent()
{
    return CYIString([UIScreen getCurrentMode]);
}

void DarkModeModulePriv::currentModeChanged(NSString *currentMode)
{
    m_pPub->OnModeChange(CYIString(currentMode));
}

 

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 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].

#ifndef _YI_ANDROID_COMMON_H_
#define _YI_ANDROID_COMMON_H_

#include <jni.h>

// Macros to simplify accessing and managing jstring objects
// usage:
//     UTF_GET_STRING(jstring);
//     UTF_STRING(jstring); //Returns UTF chars
//     UTF_RELEASE_STRING(jstring);
#define UTF_STRING(str)          sz_##str
#define UTF_GET_STRING(str) const char *UTF_STRING(str) = str ? pEnv->GetStringUTFChars((str), NULL) : nullptr
#define UTF_RELEASE_STRING(str)  {                                                          \
                                    if (str)                                                \
                                    {                                                       \
                                        pEnv->ReleaseStringUTFChars(str, UTF_STRING(str));   \
                                        pEnv->DeleteLocalRef(str);                           \
                                    }                                                       \
                                }


extern JavaVM  *cachedJVM;
extern jobject  cachedActivity;

#endif

 

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.

#ifndef _DARK_MODE_MODULE_PRIV_H_
#define _DARK_MODE_MODULE_PRIV_H_

#include "AndroidCommon.h"

class DarkModeModulePriv
{
public:
    DarkModeModulePriv();
    virtual ~DarkModeModulePriv() final;
  
    CYIString GetCurrent();

private:
    // jni objects that represent java class and instance for use in communication
    jclass m_nativeModuleClass;
    jobject m_nativeModuleInstance;
};

#endif // _DARK_MODE_MODULE_PRIV_H_

 

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.

#include "modules/DarkModeModule.h"

#include <youireact/NativeModuleRegistry.h>
#include <framework/YiAppContext.h>
#include <framework/YiApp.h>
#include <scenetree/YiSceneManager.h>

#include "DarkModeModulePriv_Android.h"

#define LOG_TAG "DarkModeModule"

YI_RN_INSTANTIATE_MODULE(DarkModeModule, yi::react::EventEmitterModule);
YI_RN_REGISTER_MODULE(DarkModeModule);

static jmethodID m_constructorMethod = 0;
static jmethodID m_getCurrentModeMethod = 0;

extern "C"
{
    JNIEXPORT void JNICALL Java_tv_youi_DarkModeModule_nativeOnThemeChange(JNIEnv *pEnv, jobject thiz, jlong nativePointer);
}

static JNIEnv *GetEnv()
{
    JNIEnv *pEnv;
    cachedJVM->GetEnv(reinterpret_cast<void**>(&pEnv), JNI_VERSION_1_6);

    return pEnv;
}

static const std::string MODE_CHANGE = "MODE_CHANGE";

DarkModeModule::DarkModeModule() : m_pPriv(new DarkModeModulePriv(this))
{
    SetSupportedEvents
    ({
       MODE_CHANGE
    });
}

DarkModeModule::~DarkModeModule() = default;

YI_RN_DEFINE_EXPORT_CONSTANT(DarkModeModule, currentMode)
{
    return folly::dynamic(m_pPriv->GetCurrent());
}

// private callback that will be invoked by friend DarkModeModulePriv when the theme changes
void DarkModeModule::OnModeChange(CYIString mode)
{
    EmitEvent(MODE_CHANGE, { mode });
}

// constructor that is responsible for initializing variables, but also setting up the JNI bridge for future use
DarkModeModulePriv::DarkModeModulePriv(DarkModeModule *pPub) :
    m_pPub(pPub),
    m_nativeModuleClass(0),
    m_nativeModuleInstance(0)
{
    JNIEnv *pEnv = GetEnv();

    if (pEnv)
    {
        if (!m_nativeModuleClass)
        {
            jclass localNativeModuleClass = pEnv->FindClass("tv/youi/DarkModeModule");
            m_nativeModuleClass = reinterpret_cast<jclass>(pEnv->NewGlobalRef(localNativeModuleClass));

            // methods
            m_constructorMethod = pEnv->GetMethodID(m_nativeModuleClass, "<init>", "(Ltv/youi/youiengine/CYIActivity;J)V");
            m_getCurrentModeMethod = pEnv->GetMethodID(m_nativeModuleClass, "GetCurrentMode", "()Ljava/lang/String;");

            // create an instance using the constructor
            jobject localNativeModule = pEnv->NewObject(m_nativeModuleClass, m_constructorMethod, cachedActivity, (jlong)this);
            YI_ASSERT(localNativeModule, LOG_TAG, "Failed to instance NativeModuleClass.");

            m_nativeModuleInstance = pEnv->NewGlobalRef(localNativeModule);
        }
    }
}
DarkModeModulePriv::~DarkModeModulePriv() = default;

// retrieve the current theme via sync JNI method call
CYIString DarkModeModulePriv::GetCurrent()
{
    JNIEnv *pEnv = GetEnv();
    jstring jCurrentMode = (jstring)pEnv->CallObjectMethod(m_nativeModuleInstance, m_getCurrentModeMethod);

    // trasnform the jstring into a CYIString and remove its ref to clear memory
    UTF_GET_STRING(jCurrentMode);
    CYIString currentMode(UTF_STRING(jCurrentMode));
    UTF_RELEASE_STRING(jCurrentMode);

    return currentMode;
}

// callback to be invoked by externed JNI native method
void DarkModeModulePriv::onThemeChange()
{
    // notify our DarkModeModule as a friend that theme has changed
    m_pPub->OnModeChange(GetCurrent());
}

// JNI native method that can be invoked by java code
JNIEXPORT void JNICALL Java_tv_youi_DarkModeModule_nativeOnThemeChange(JNIEnv *pEnv, jobject thiz, jlong nativePointer)
{
    YI_UNUSED(thiz);
    YI_UNUSED(pEnv);

    // use the event emitter to notify RN layer that something has occurred and complete circle
    DarkModeModulePriv *pDarkModeModulePriv = reinterpret_cast<DarkModeModulePriv *>(nativePointer);
    if (pDarkModeModulePriv) {
        pDarkModeModulePriv->onThemeChange();
    }

}

 

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

package tv.youi;
import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
import tv.youi.youiengine.CYIActivity;

public class DarkModeModule implements CYIActivity.LifecycleListener {

    private long nativePointer = 0;
    private CYIActivity mainActivity;

    private class Receiver extends BroadcastReceiver {
        private DarkModeModule module;

        public Receiver(DarkModeModule module) {
            super();
            this.module = module;
        }

        @Override
        public void onReceive(Context context, Intent intent) {
            this.module.notifyForChange();
        }
    }

    public DarkModeModule(CYIActivity activity, final long nativePointer)
    {
        // this native pointer provides a way to callback to your native C++ code
        this.nativePointer = nativePointer;

        // this is a reference to your main activity to provide you context if needed
        this.mainActivity = activity;

        // register as a receiver for configuration changes
        this.mainActivity.getBaseContext().registerReceiver(new Receiver(this), new IntentFilter("android.intent.action.CONFIGURATION_CHANGED"));
    }

    /**
     * this is a standard void method. If this returned a value, it would occur synchronously.
     */
    public String GetCurrentMode()
    {
        int currentMode = this.mainActivity.getBaseContext().getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
        if (currentMode == Configuration.UI_MODE_NIGHT_YES)
        {
            return "dark";
        }
        return "light";
    }

    public void notifyForChange()
    {
        // invoke native JNI method with pointer that references DarkModeNativePriv
        nativeOnThemeChange(this.nativePointer);
    }

    public void onActivityPaused(CYIActivity activity)
    {
        // noop
    }

    public void onActivityResumed(CYIActivity activity)
    {
        this.notifyForChange();
    }

    native void nativeOnThemeChange(long nativePointer);

}

 

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.

#include "DarkModeModule.h"

#include <youireact/NativeModuleRegistry.h>
#include <framework/YiAppContext.h>
#include <framework/YiApp.h>
#include <scenetree/YiSceneManager.h>

#include "DarkModeModulePriv_Default.h"

#define LOG_TAG "DarkModeModule"

YI_RN_INSTANTIATE_MODULE(DarkModeModule, yi::react::EventEmitterModule);
YI_RN_REGISTER_MODULE(DarkModeModule);

static const std::string MODE_CHANGE = "MODE_CHANGE";

DarkModeModule::DarkModeModule() : m_pPriv(new DarkModeModulePriv)
{
    SetSupportedEvents
    ({
       MODE_CHANGE
    });
}

DarkModeModule::~DarkModeModule() = default;

YI_RN_DEFINE_EXPORT_CONSTANT(DarkModeModule, currentMode)
{
    return folly::dynamic(m_pPriv->GetCurrent());
}

DarkModeModulePriv::DarkModeModulePriv() = default;
DarkModeModulePriv::~DarkModeModulePriv() = default;

CYIString DarkModeModulePriv::GetCurrent()
{
    // for platforms that do not have the notion of dark mode, let's default to light
    return CYIString("light");
}

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.

Here’s something similar we think you’ll enjoy.