Extending the API

This page shows how to extend the ApertusVR Java API, if needed.

Overview

It may occour, that you want to extend the API, becouse there are entities with missing wrappers, or you wrote new ApertusVR entity, or feature. We will shortly discuss, what you have to do, to make your existing C++ feature in ApertusVR to work on Android using Java.

In this page, we will go through step-by-step on the extension process using the ConeGeometry entity as an example.

There are three places, where you have to make modifications:

  1. ApertusJNI.java, where you have to register new native functions

  2. apeJNIPlugin shared library, where you have to include new .cpp files, and implement the registered functions

  3. org.apertusvr Java-package, where you have to create new wrappers

ApertusJNI

For each member function in the ape::IConeGeometry interface, we have to register a static native Java-function in the ApertusJNI.java file. So take a glance at the IConeGeometry interface:

class IConeGeometry : public ape::Geometry
{
protected:
		IConeGeometry(std::string name, bool replicate, std::string ownerID) : ape::Geometry(name, ape::Entity::GEOMETRY_CONE, replicate, ownerID) {}

		virtual ~IConeGeometry() {};

public:
		virtual void setParameters(float radius, float height, float tile, ape::Vector2 numSeg) = 0;

		virtual ape::GeometryConeParameters getParameters() = 0;

		virtual void setParentNode(ape::NodeWeakPtr parentNode) = 0;

		virtual void setMaterial(ape::MaterialWeakPtr material) = 0;

		virtual ape::MaterialWeakPtr getMaterial() = 0;

		virtual void setOwner(std::string ownerID) = 0;

		virtual std::string getOwner() = 0;
};

We are concerned only about the public virtual function. The convention is that we indicate the entity type in the static native function. So in the ConeGeometry case, it looks like this:

static native void setConeGeometryParameters(String nativeCone, float radius, float height, float tile, float numSegX, float numSegY);

static native @Size(5) float[] getConeGeometryParameters(String nativeCone);

static native void setConeGeometryParentNode(String nativeCone, String parentNode);

static native void setConeGeometryMaterial(String nativeCone, String material);

static native String getConeGeometryMaterial(String nativeCone);

static native void setConeGeometryOwner(String nativeCone, String ownerID);

static native String getConeGeometryOwner(String nativeCone);

After the set, get, is etc., comes the entity name, then the "subject" (e.g. ParentNode). Comparing the two set of function headers, note that the rules are:

  • std::string turns into String

  • ape::Entity types turn into String, as we access them by their IDs

  • ape::Vector2, ape::Color, etc. turns into separate float values, as we pursue to use primitive types whenever it is possible.

  • enum types turn into int variables

Now, we are done with the ApertusJNI file, we can go forward the next step.

apeJNIPlugin

The JNI-functions are contained in the apeJNIPlugin shared library. For each entity and other component (e.g. ape::CoreConfig) there is a corresponding apeJNI<entityName>.cpp file (e.g. apeJNICoreConfig.cpp).

This will be the case in the new ConeGeometry entity, we create an apeJNIConeGeometry.cpp file, where we place the JNI-functions. The rule is that for a static native function in the packaganame package, and className class with functionName name, we have its corresponding JNI-function header as:

extern "C"
JNIEXPORT /* ourParam */ JNICALL
Java_packagename_className_functionName(JNIEnv *env, jclass clazz, /* parameters */);

E.g. our first function's header will look like:

extern "C"
JNIEXPORT void JNICALL
Java_org_apertusvr_ApertusJNI_setConeGeometryParameters(
    JNIEnv *env, jclass clazz, jstring native_cone,
    float radius, float height, float tile, 
    float num_seg_x, float num_seg_y);

Next task is to implement the function. We will only show the first two, but you can find all of them on GitHub.

JNI-functions are traditionally C-like functions, as the whole API is designed as a C API (even though we can use any C++ feature in JNI-functions). So how do we access the ape::ISceneManager, to query and modify our entities? We use the ape::JNIPlugin as a crutch.

This plugin is a special one in the term of that it acts as a singleton. Therefore we can get a pointer pointing to the plugin (which has access to the whole ApertusVR system) with the ape::JNIPlugin::getPluginPtr() static function. So we query this pointer, and store it to a variable. Then we convert the entity id into a C-style string with the env pointer:

const char* name = env->GetStringUTFChars(native_cone, nullptr);

At this point we have the id, and we have a pointer pointing to the JNIPlugin, which has access to the sceneManager. This means we can do anything we want! Okay let us move on. As we are implementing the setParameters(...) function our task is to set the parameters for the cone with the id name. This goes naturally, just like we use the ApertusVR C++ API:

if(auto coneGeometryShared = std::static_pointer_cast<ape::IConeGeometry>(jniPlugin->getSceneManager()->getEntity(std::string(name)).lock()))
{
    coneGeometryShared->setParameters(radius, height, tile, ape::Vector2(num_seg_x,num_seg_y));
}

Great! Now we are finished with the task, then we only have to clean the mess after us, so we just release the UTF string we recently allocated:

env->ReleaseStringUTFChars(native_cone, name);

Always call the ReleaseStringUTFChars(...) function for every GetStringUTFChars(...) allocation before leaving the function, otherwise it will cause memory leak, just like when somebody forgot to call delete after new.

What if we have to return some value? Let us see the next function: getParameters(...). The first half of the procedure goes exactly the same as in the case when we are not interested in any returning value. However, when we are, then we should ask for it on the same place where we set the parameters in the previous example:

ape::GeometryConeParameters coneParameters;
if(auto coneGeometryShared = std::static_pointer_cast<ape::IConeGeometry>(jniPlugin->getSceneManager()->getEntity(std::string(name)).lock()))
{
    coneParameters = coneGeometryShared->getParameters();
}

Good. Then we release the allocated utf chars, and then we can return a value. As it was said, our goal is to always cope with primitve values when it is possible, becouse calling Java constructors from JNI-functions can be really expensive. Thus complex types (like ape::GeometryConeParameters) are returned as arrays. So in our case this complex type in C++ looks like the following struct:

struct GeometryConeParameters
{
	float radius;
	float height;
	float tile;
	ape::Vector2 numSeg;

	/* constructors, etc. ... */
};

This means we will return 3 + 2 float value. So first we create a buffer array:

float outArrayBuf[] = {
    coneParameters.radius,
    coneParameters.height,
    coneParameters.tile,
    coneParameters.num_seg_x,
    coneParameters.num_seg_y
};

Then we create a new jfloatArray with the help of the env pointer, and then place this buffer into the newly create jfloatArray, and then just return it:

jfloatArray jOutArray = env->NewFloatArray(5);
env->SetFloatArrayRegion(jOutArray,0,5,outArrayBuf);

return jOutArray;

Now do this for each of the member functions. Then if it is done, one last thing remained: we have to include our new apeJNIConeGeometry.cpp file into the cmake project.

To do this open the CMakeLists.txt file, and add the apeJNIConeGeometry.cpp to the SOURCE list:

set(SOURCES
        /* ...
         * other cpp files
         * ...
         */
        apeJNIConeGeometry.cpp
        )

Creating wrapper class

Wrapper classes are stored in the org.apertusvr package. So navigate to the corresponding folder, and create the new file for the wrapper class, as apeConeGeometry.java!

In the C++ API the ape::IConeGeometry is a subclass of the ape::Geometry interface. We have to follow this structure here too. We assume that somebody already created the apeGeometry wrapper for us, so we just type:

apeConeGeometry.java
package org.apertusvr;

public class apeConeGeometry extends apeGeometry {
    // ...
}

Parameter types

For the complex parameter types in the C++ API, we often create a similar complex type as the subclass of the entity interface. In our case this will look like:

public class GeometryConeParameters
{
    public float radius;
    public float height;
    public float tile;
    public apeVector2 numSeg;

    /* ordinal constructors */ 

    GeometryConeParameters(@Size(5)float[] paramArray) {
        radius = paramArray[0];
        height = paramArray[1];
        tile = paramArray[2];
        numSeg = new apeVector2(paramArray[3],paramArray[4]);
    }
}

We always make a constructor whose parameter is an array if the values are from the same primitive type. This makes it easier to constructing from the returned array from ApertusJNI.

Constructor

The interfaces only store the name and the type of the given entity. This two value are also an obligatory, otherwise we could not identify the entities without them. Thus, we only write constructors where this two parameters are given to the apeEntity superclass. In our case, we now that we are working with a ConeGeometry, so we make a constructor with only one String parameter, and call the superclass's constructor with this String and the apeEntity.Type.GOMETRY_CONE value:

public apeConeGeometry(String name) {
    super(name, Type.GEOMETRY_CONE);
}

Member functions

The wrapper's member functions simply call the corresponding static native function in ApertusJNI, with the parameters they got from the user, and with the name which is stored in the wrapper. So in the case of the two function whose implementation were discussed in the previous section, looks like this:

public void setParameters(float radius, float height, float tile, apeVector2 numSeg) {
    ApertusJNI.setConeGeometryParameters(mName,radius,height,tile,numSeg.x, numSeg.y);
}

public GeometryConeParameters getParameters() {
    return new GeometryConeParameters(ApertusJNI.getConeGeometryParameters(mName));
}

The header of the wrapper's member function are always the some (or equivalent) as in the C++ API. This means here we do not use separate float x and float y, but an apeVector2 (which is part of the Java API).

Builder class

For entities like apeConeGeometry, we should always provide a Builder class, which is an implementation of the apeBuilder interface. This is really important if we want to cast from an apeEntity or other superclass, because we have to instantiate one for it.

This builder class is responsible for creating an instance of the wrapper when it appears as a template parameter. So without further explanation, the builder class looks like this:

public static class apeConeBuilder implements apeBuilder<apeConeGeometry> {

    @Override
    public apeConeGeometry build(String name, Type type) {
        if (type == Type.GEOMETRY_CONE) {
            return new apeConeGeometry(name);
        }

        return null;
    }
}

Last updated