Skip to content

Native methods

Allan C. M. Lira edited this page Nov 26, 2020 · 9 revisions

Table of contents

Syntax

TotalCross has a certain syntax for native methods to be written

Java level

The Java method can be either marked as native or have a Java implementation and the annotation

@ReplacedByNativeOnDeploy
static String getMyString() {

...

the annotation exists so we can provide a java implementation when running on the Launcher, using the JDK itself, and a native implementation when running on the device, without resorting to template methods, polymorphism or whatever.

C level

The syntax of the C function that is mapped to the java method is the following:

<first-letter-of-each-package-in-lowercase><camel-case-characters-of-class>_<method-name>_<first-letter-of-each-type-argument>(NMParams p)

for example:

package totalcross.ui.image;

...

public class Image {

...

  @ReplacedByNativeOnDeploy
  final public void applyColor(int color) {

...

becomes:

tuiI_applyColor_i(NMParams p);

Arguments

There are some details about method arguments:

Type Variable notation Array notation
int i I
long l L
char c C
double d D
boolean z Z
byte b B
String s S
Object o O

Therefore:

  • Upper case for arrays;
  • Functions without arguments doesn't need the last _ term;
  • TotalCross don't support native float, but you can workaround with double;
  • Methods with f like tsC_getBreakPos_fsiib:
    getBreakPos(FontMetrics fm, StringBuffer sb, int start, int width, boolean doWordWrap)
    are just object references;
  • The class name is formed by camel case, so NotificationManager becomes NM, NativeDB becomes NDB, etc.

Return

Look:

TC_API void tdsNDB_shared_cache_b(NMParams p) // totalcross/db/sqlite/NativeDB native int shared_cache(boolean enable);
{
   TRACE("tdsNDB_shared_cache_b")
   bool enable = p->i32[0];
   LOCKDB
   p->retI = sqlite3_enable_shared_cache(enable);
   UNLOCKDB
}

In this section we can see the access to the values in p. They are accessed as an array, if there was one more integer in return it could be obtained as follows:

int oneMore = p->i32[1];

To return an integer to Java use p->retI = something you can change the last letter following the convention to return other types.

Runtime

Those functions are all defined at compile time and depending on the platform, they are loaded using dlsym or using a C hashtable that contains a pointer for each function mapped to their name. Since they all share the same signature, it's always the same function call:

void tsC_getBreakPos_fsiib(NMParams p)

That's how we do it without ASM: NMParams is used to pass the actual arguments and store the return value.

Ok, but how is the actual method selected from the hash table?

If you look at tcvm.c, around line 636, there's a label nativeMethodCall, when this flag is set:

if (method->flags.isNative)

The code jumps to this label and finds the correct function using newMethod->nativeSignature depending on the platform, the function findProcAddress is either a dlsym or a hashtable.get using this, it later calls newMethod->boundNM(nmp).

Writing your native methods

You need:

  1. Create your Java interface;
  2. Create your native implementation in a .c or .cpp file and pay attention to include tcvm.h;
  3. Add your native file to the build list: totalcross/TotalCrossVM/CMakeLists.txt;
  4. Add your interfaces to the TotalCrossVM/src/nm/NativeMethods.txt file:
    totalcross/sys/Vm|native public static void alert(String s);
    <path-to-your-java-file>|native <your function>;

Then just generate the new files, both VM and Java SDK (API).

Sometimes you have to add your native interfaces to the TotalCrossVM/src/nm/NativeMethodsPrototypes.txt file too!