Tutorial #00: Custom Control

If a required platform-dependent feature is not supported by the Murl Engine, a custom control can be used to add this feature. This tutorial shows with a fairly simple example, how you can implement your own custom controls.

Quick links to the individual sections in this tutorial:

Base Class

All control classes implement the IControlable interface with the methods:

  • GetName()
  • FrameUpdate()
  • LogicUpdate()
  • ConfigChanged()

This applies to custom controls as well as to provided controls like the IMusicPlayerControl, the IRumbleControl, the IWebControl, the IScreenshotControl etc.

Custom controls additionally implement the ICustomControlable interface with the methods:

  • Init()
  • DeInit()
  • PauseEngine()
  • ContinueEngine()
  • SuspendEngine()
  • ResumeEngine()
  • ...

The class CustomControlable provides an empty implementation of both interfaces and can be used as base class for custom controls.

MyControl

For our simple custom control we create a new abstract class MyControl with CustomControlable as base class and the two pure virtual methods Test() and TestBack(). Additionally we define the static method Create() which we will use later to create a MyControl object.

#include "murl_custom_controlable.h"

namespace Murl
{
    namespace App
    {
        class MyControl : public CustomControlable
        {
        public:
            static MyControl* Create(const String& name);

            MyControl(const String& name) : CustomControlable(name) {}

            virtual ~MyControl() {}

            virtual Bool Test(const String& text) = 0;
            virtual Bool TestBack(const String& text) = 0;
        };
    }
}

To simplify matters we implement the empty constructor and the empty destructor directly in the header file.

Registration

We are now able to create an object of our MyControl class and to register the object with the Output::IDeviceHandler. Ideally we do this in the OnInit() method of our logic class.

Logic::IDeviceHandler* deviceHandler = state->GetDeviceHandler();
Output::IDeviceHandler* outputDeviceHandler = deviceHandler->GetOutputDeviceHandler();

// create MyControl instance
mMyControl = MyControl::Create("MyControl");

// add custom control to outputDeviceHandler
if (!outputDeviceHandler->AddCustomControl(mMyControl))
{
    return false;
}

After the registration the methods of the IControlable and CustomControlable interface are called on their respective events.

We call the methods Test() and TestBack() if the corresponding button (or key) has been pressed.

if (deviceHandler->WasRawKeyPressed(RAWKEY_1) || mButton01->WasReleasedInside())
{
    mMyControl->Test("b1 pressed");
}

if (deviceHandler->WasRawKeyPressed(RAWKEY_2) || mButton02->WasReleasedInside())
{
    mMyControl->TestBack("b2 pressed");
}

We also have to deregister the custom control object in the OnDeInit() method. This can be done with the reference mMyControl or with the name specified during creation.

Logic::IDeviceHandler* deviceHandler = state->GetDeviceHandler();
Output::IDeviceHandler* outputDeviceHandler = deviceHandler->GetOutputDeviceHandler();

// remove custom control from outputDeviceHandler
if (!outputDeviceHandler->RemoveCustomControl(mMyControl))
{
    return false;
}

Default Implementation

For the new abstract class we create a default implementation in the class MyControlDefault.

#include "my_control.h"

namespace Murl
{
    namespace App
    {
        class MyControlDefault : public MyControl
        {
        public:
            MyControlDefault(const String& name);
            virtual ~MyControlDefault();

            virtual Bool Test(const String& text);
            virtual Bool TestBack(const String& text);
        };
    }
}

No additional methods (Init(), DeInit(), FrameUpdate() ...) are needed for the default implementation.

The Create method creates a new MyControlDefault object and returns a pointer to the new object.

#include "my_control_default.h"

using namespace Murl;

App::MyControl* App::MyControl::Create(const String& name)
{
    return new App::MyControlDefault(name);
}

App::MyControlDefault::MyControlDefault(const String& name)
: MyControl(name)
{}

App::MyControlDefault::~MyControlDefault()
{}

The two methods Test() and TestBack() just print a debug message.

Bool App::MyControlDefault::Test(const String& text)
{
    MURL_TRACE(0, "text=\"%s\"", text.Begin());
    return true;
}

Bool App::MyControlDefault::TestBack(const String& text)
{
    MURL_TRACE(0, "text=\"%s\"", text.Begin());
    return true;
}

This default implementation can be used on all platforms where no specific implementation exists.

Android Implementation

Next we implement an Android implementation in the class MyControlAndroid.

The method Test() should call a Java method with the same name. Additionally we define a C++ method which is called from Java.

Note
Calls from C++ to Java and vice versa are done through the Java Native Interface (JNI) - see also Java Native Interface Specification

The header for Android is similar to the class MyControlDefault but we additionally need the methods Init() and DeInit() as well as two member variables for the NativePlatform object and the JniBridge object.

#include "my_control.h"
#include "murl_platform_android_native_platform.h"
#include "murl_platform_android_jni_bridge.h"

namespace Murl
{
    namespace App
    {
        class MyControlAndroid : public MyControl
        {
        public:
            MyControlAndroid(const String& name);
            virtual ~MyControlAndroid();

            virtual Bool Test(const String& text);
            virtual Bool TestBack(const String& text);

        protected:
            virtual Bool Init(IPlatform* platform);
            virtual Bool DeInit();

            Platform::Android::NativePlatform* mPlatform;
            Platform::Android::JniBridge* mJniBridge;           
        };
    }
}

We again use the Create() method to create a new MyControlAndroid object.

#include "my_control_android.h"

using namespace Murl;

App::MyControl* App::MyControl::Create(const String& name)
{
    return new App::MyControlAndroid(name);
}

App::MyControlAndroid::MyControlAndroid(const String& name)
: MyControl(name)
{
}

App::MyControlAndroid::~MyControlAndroid()
{
}

The Init() method is used to get pointers to the JniBridge and the NativePlatform. Additionally we store a pointer to the MyControlAndroid object in the static variable sMyControl.

static App::MyControl* sMyControl = 0;

Bool App::MyControlAndroid::Init(IPlatform* platform)
{
    mPlatform = dynamic_cast<Platform::Android::NativePlatform*>(platform);
    if (mPlatform == 0)
    {
        MURL_TRACE(0, "Failed to get Android platform handle.");
        return false;
    }

    mJniBridge = mPlatform->GetJniBridge();
    if (mJniBridge == 0)
    {
        MURL_TRACE(0, "Failed to get JNI bridge.");
        return false;
    }

    sMyControl = this;

    return true;
}

Bool App::MyControlAndroid::DeInit()
{
    sMyControl = 0;
    return true;
}

Cpp2Java

The class JniBridge provides useful template methods to easily call static Java methods:

  • CallStaticJavaProc
  • CallStaticJavaIntFunc
  • CallStaticJavaObjectFunc

The methods are defined in the file murl_platform_android_jni_bridge.h:

  • murl/base/source/platform/android/murl_platform_android_jni_bridge.h

We use the template method CallStaticJavaProc to call a Java method without return parameter.

The template parameters define the number and type of the parameters for the Java method (<jlong, jstring>).

The first function parameter defines the Java method. Per definition, this is the name of the Java class followed by a point and the name of the Java method ("MyTestClass.Test").

As second parameter we assign a platform handle (mPlatform->GetHandle()) and as third parameter a String ("Greetings from CPP land!").

Note
The platform handle parameter is for demonstration purpose only and is not really required for this example. The handle can be used in the Java method to get a reference of the MurlPlatform object, which provides methods to access the Android activity, Android context etc.
Bool App::MyControlAndroid::Test(const String& text)
{
    MURL_TRACE(0, "text=%s", text.Begin());
    mJniBridge->CallStaticJavaProc<jlong, jstring>("MyTestClass.Test", mPlatform->GetHandle(), "Greetings from CPP land!");
    return true;
}

The corresponding Java method:

public class MyTestClass
{
// JNI from C++ to Java
public static void Test(long platformPtr, String text)
{
...
}

We store the Java file in the directory

  • source/android/java/com/mycompany/mystuff/MyTestClass.java

The Java method Test() prints debug messages with Log.d() and calls afterwards the method TestBack().

// Copyright 2015 Spraylight GmbH
package com.mycompany.mystuff;
import at.spraylight.murl.MurlPlatform;
import at.spraylight.murl.MurlJniBridge;
import android.util.Log;
public class MyTestClass
{
// JNI from C++ to Java
public static void Test(long platformPtr, String text)
{
Log.d("Murl", "MyTestClass::Test(): text=" + text);
MurlPlatform platform = MurlJniBridge.GetJavaPlatform(platformPtr);
if (platform == null)
{
Log.d("Murl", "MyTestClass::Test(): failed to get Java platform.");
return;
}
Log.d("Murl", "MyTestClass::Test(): Java platform=" + platform.toString());
Log.d("Murl", "Calling TestBack() ...");
TestBack("Greetings from Java land!");
}
// JNI from Java to C++
public static native void TestBack(String text);
}

To add the Java file to the build process we need to specify it in the Makefile with MURL_ANDROID_JAVA_FILES. Additionally we need to specify the Java class with MURL_ANDROID_JNI_CLASSES as JNI class to register it with the JNI bridge.

MURL_ANDROID_JAVA_PATH := source/android/java
MURL_ANDROID_JAVA_FILES += com/mycompany/mystuff/MyTestClass.java
MURL_ANDROID_JNI_CLASSES += com/mycompany/mystuff/MyTestClass
Note
It is also possible to specify Java archives (JAR files) in the Makefile with MURL_ANDROID_JAR_FILES.
MURL_ANDROID_JAR_PATH := source/android/jar
MURL_ANDROID_JAR_FILES += myArchive.jar

Java2Cpp

To call a native C++ method from Java, we need to create a Java method and mark it as native.

// JNI from Java to C++
public static native void TestBack(String text);

The corresponding function in C++ is named Java_com_mycompany_mystuff_MyTestClass_TestBack().

extern "C" JNIEXPORT void JNICALL Java_com_mycompany_mystuff_MyTestClass_TestBack(JNIEnv * env, jobject obj, jstring text)
{
    String textStr;
    Platform::Android::JniData::GetCParam(env, textStr, text);

    MURL_TRACE(0, "text=%s", textStr.Begin());

    if (sMyControl == 0)
    {
        MURL_TRACE(0, "Java_com_mycompany_mystuff_MyTestClass_TestBack(): No test control");
        return;
    }

    sMyControl->TestBack(textStr);
}

The statement extern "C" instructs the compiler to export the function according to the C specification (instead of the C++ specification). JNIEXPORT and JNICALL are JNI macros used to ensure that the corresponding function is exported with appropriate linkage.

The naming convention for the C++ function is Java_{package_and_classname}_{function_name}(JNI arguments) with dots replaced by underscores.

The first parameter is always a reference to the JNI environment, which lets you access all the JNI functions. The second parameter is always a reference to the this Java object. The third parameter is the first parameter of the Java method. In our case this is a Java-String object (jstring).

The function GetCParam converts the jstring into a Murl Engine String. This and other functions to convert Java data types to C++ data types and vice versa are defined in the file murl_platform_android_jni_data.h.

  • murl/base/source/platform/android/murl_platform_android_jni_data.h

The static variable sMyControl can be used to access the MyControlAndroid object and to call the actual method TestBack().

The Android implementation is now complete.

Depending on the platform, we use either the default implementation or the Android implementation. Thus, we have to exclude the Android implementation in the Visual Studio and Xcode projects. For Android we have to exclude the default implementation.

To do so, we adjust the common Makefile:

ifeq ($(MURL_OS), android)
MURL_MODULE_SRC_FILES += my_control_android.cpp
else
MURL_MODULE_SRC_FILES += my_control_default.cpp
endif

When the button b1 is pressed we get the expected output:

Murl::App::MyControlAndroid::Test(), line 52: text=b1 pressed
MyTestClass::Test(): text=Greetings from CPP land!
MyTestClass::Test(): Java platform=at.spraylight.murl.MurlPlatform@4051b6d0
Calling TestBack() ...
TestBack(), line 62: text=Greetings from Java land!
Murl::App::MyControlAndroid::TestBack(), line 76: text=Greetings from Java land!

Proguard

For debug builds the Android implementation is working nicely. But when doing a release build, the Java optimizer Proguard would optimize away our Java class because it looks unused.

Therefore we have to instruct the tool to keep our Java class in any case. To do so we create a text file proguard_MyTestClass.cfg

  • source/android/proguard_MyTestClass.cfg

and put a keep option for proguard into the file:

-keep class com.mycompany.mystuff.MyTestClass { *; }

To add the file to the build process, we need to specify it in the Makefile as Proguard fragment.

MURL_ANDROID_PROGUARD_PATH := source/android/
MURL_ANDROID_PROGUARD_FRAGMENTS += proguard_MyTestClass.cfg

Summary

Cpp2Java

// CPP
mJniBridge->CallStaticJavaProc<jlong, jstring>("MyTestClass.Test", mPlatform->GetHandle(), "Greetings from CPP land!");
// Java
public static void Test(long platformPtr, String text)

Java2Cpp

// Java
public static native void TestBack(String text);
// CPP
extern "C" JNIEXPORT void JNICALL Java_com_mycompany_mystuff_MyTestClass_TestBack(JNIEnv * env, jobject obj, jstring text)

Makefile

ifeq ($(MURL_OS), android)
MURL_MODULE_SRC_FILES += my_control_android.cpp
else
MURL_MODULE_SRC_FILES += my_control_default.cpp
endif
MURL_ANDROID_JAVA_PATH := source/android/java
MURL_ANDROID_JAVA_FILES += com/mycompany/mystuff/MyTestClass.java
MURL_ANDROID_JNI_CLASSES += com/mycompany/mystuff/MyTestClass
MURL_ANDROID_PROGUARD_PATH := source/android/
MURL_ANDROID_PROGUARD_FRAGMENTS += proguard_MyTestClass.cfg
MURL_ANDROID_JAR_PATH := source/android/jar
MURL_ANDROID_JAR_FILES += myArchive.jar

OSX Implementation

In the same way, we can create implementations for Windows, Linux, OSX or iOS. For those platforms the platform dependent code can be directly written in the C++ files. There is no need to use a JNI bridge like on Android.

As an example we show an OSX implementation. The class MyControlOsx is declared in the header file my_control_osx.h.

#include "my_control.h"

namespace Murl
{
    namespace App
    {
        class MyControlOsx : public MyControl
        {
        public:
            MyControlOsx(const String& name);
            virtual ~MyControlOsx();

            virtual Bool Test(const String& text);
            virtual Bool TestBack(const String& text);
        };
    }
}

The implementation of the class is done in the Objective-C++ file my_control_osx.mm. In this file we can use C++ code as well as Objective-C code.

We import the file Foundation.h to be able to use the Objective-C method NSLog(). The method Create() is again used to create a MyControlOsx object.

#include "my_control_osx.h"
#import <Foundation/Foundation.h>

using namespace Murl;

App::MyControl* App::MyControl::Create(const String& name)
{
    return new App::MyControlOsx(name);
}

App::MyControlDefault::MyControlDefault(const String& name)
: MyControl(name)
{}

App::MyControlDefault::~MyControlDefault()
{}

The method Test() and TestBack() use NSLog() to print a message.

Bool App::MyControlOsx::Test(const String& text)
{
    NSString* nsString = [NSString stringWithUTF8String:text.Begin()];
    NSLog(@"text=%@", nsString);
    return true;
}

Bool App::MyControlOsx::TestBack(const String& text)
{
    NSString* nsString = [NSString stringWithUTF8String:text.Begin()];
    NSLog(@"text=%@", nsString);
    return true;
}

We need to include these two files to the OSX Xcode project and exclude the files from the default and Android implementation.

Screenshot

tut0400_custom_control.png
Custom Control


Copyright © 2011-2024 Spraylight GmbH.