-
Notifications
You must be signed in to change notification settings - Fork 133
The Bro Java to Native Bridge
Bro is a RoboVM specific API that enables Java code to call directly into native code without using JNI. It has been inspired by JNA, BridJ and .NET's P/Invoke.
Bro is the Swedish word for bridge and is pronounced broo.
We'll introduce bro with a simple example:
import org.robovm.rt.bro.*;
import org.robovm.rt.bro.annotation.*;
@Library("c") // [1]
public class Abs {
static {
Bro.bind(); // [3]
}
@Bridge private static native int abs(int i); // [2]
public static void main(String[] args) {
System.out.println(abs(-100));
}
}
-
The
@Library
annotation tells Bro to look for symbols in thelibc
system library. Bro will prependlib
and append.so
to the specified library name when searching for a matching library -
The
@Bridge
annotatedAbs.abs()
method will bind to the nativeabs()
function inlibc
. -
The
Bro.bind()
call initiates the lookup of native functions and binds them to the corresponding@Bridge
annotated methods. This process relies ondlopen()
anddlsym()
to find matching symbols.
Native integer types (e.g. char
, int32_t
, float
) are mapped to Java integer types of the same size:
Java | Bits | Example C type * |
---|---|---|
byte | 8 | char, uint8_t |
short | 16 | short |
char | 16 | uint16_t |
int | 32 | int, int32_t |
long | 64 | long long, int64_t |
float | 32 | float |
double | 64 | double |
* Depends on compiler and platform
NOTE: Unsigned types (e.g.
uint32_t
) are mapped to the signed Java type of the same bit size.
Some native types have varying bit sizes depending on the target platform. E.g. the C long
type is usually a 32-bit integer on 32-bit platforms but a 64-bit integer on 64-bit platforms. To support such types Bro provides three different annotations:
@MachineSizedFloat
is used to bind floating point types which are 32-bit on 32-bit platforms and 64-bit on 64-bit platforms. The CGFloat
type in Apple's Cocoa and CocoaTouch APIs is an example of such a type. CGFloat
is bound using @MachineSizedFloat double
in RoboVM's CocoaTouch bindings. On the Java side the value is kept in a float
or double
and Bro will take care of the native<-->Java conversion.
The C function which returns x^y^ of a CGFloat
value
CGFloat pow(CGFloat x, CGFloat y);
would be bound like this using Bro:
@Bridge
public native @MachineSizedFloat double pow(
@MachineSizedFloat double x,
@MachineSizedFloat double y);
NOTE: The method parameter or return type annotated with
@MachineSizedFloat
must either have the Java typefloat
ordouble
.
CAUTION: When using
@MachineSizedFloat float
on 64-bit platforms Bro will cast the native value from a 64-bitdouble
to a 32-bitfloat
value when converting a native value to a Java value. This operation may result in loss of precision. The same situation occurs when using@MachineSizedFloat double
on 32-bit platforms and passing a Java value to native code.
@MachineSizedSInt
is used to bind signed integer types which are 32-bit on 32-bit platforms and 64-bit on 64-bit platforms. The NSInteger
type in Apple's Cocoa and CocoaTouch APIs is an example of such a type. NSInteger
is bound using @MachineSizedSInt long
in RoboVM's CocoaTouch bindings. On the Java side the value is always kept in a long
and Bro will take care of the native<-->Java conversion.
The C function which returns the asbolute of an NSInteger
value
NSInteger abs(NSInteger v);
would be bound like this using Bro:
@Bridge
public native @MachineSizedSInt long abs(
@MachineSizedSInt long v);
NOTE: The method parameter or return type annotated with
@MachineSizedSInt
must have the Java typelong
.
@MachineSizedUInt
is used to bind signed integer types which are 32-bit on 32-bit platforms and 64-bit on 64-bit platforms. The NSUInteger
type in Apple's Cocoa and CocoaTouch APIs is an example of such a type. NSUInteger
is bound using @MachineSizedUInt long
in RoboVM's CocoaTouch bindings. On the Java side the value is always kept in a long
and Bro will take care of the native<-->Java conversion.
The C function which returns the max of two NSUInteger
values
NSUInteger max(NSUInteger a, NSUInteger b);
would be bound like this using Bro:
@Bridge
public native @MachineSizedUInt long max(
@MachineSizedUInt long a,
@MachineSizedUInt long b);
NOTE: The method parameter or return type annotated with
@MachineSizedUInt
must have the Java typelong
.
Pointers are passed as Java long
values annotated with the Bro @Pointer
annotation. The Bro compiler will handle the 64-bit <--> 32-bit conversions on 32-bit platforms.
The C malloc()
function
void *malloc(size_t size);
could be bound like this using Bro:
@Bridge
public native @Pointer long malloc(
@MachineSizedUInt long size);
Bro provides special pointer classes for each of the Java primitive types which makes it easier to work with pointers to primitive types. Using these classes facilitates converting between Java arrays of primitives and native memory, converting pointers to direct java.nio.Buffer
instances and more. These classes are located in the org.robovm.rt.bro.ptr
package.
The C getenv()
function
char *getenv(const char *name);
can be bound like this using Bro in RoboVM:
@Bridge
public native BytePtr getenv(BytePtr name);
And used like this to print out the value of $HOME
:
public static void main(String[] args) {
BytePtr value = getenv(BytePtr.toBytePtrAsciiZ("HOME"));
System.out.println(value.toStringAsciiZ());
}
C struct
types are mapped to Java by extending the bro Struct
class. Each member of the C struct
is bound using a getter method and a setter method that must be native
and annotated with the bro @StructMember
annotation. The @StructMember
annotation specifies the index of the member in the struct
. The getter method must take 0 parameters and return some value while the setter method must take 1 parameter of the same type as the getter returns. The return type for the setter must either be void
or the Struct
class it belongs to.
TIP: The names of the getter and setter methods don't have to follow the Java Beans style convention for Java Beans properties.
TIP: If the setter method is declared as returning an instance of the
Struct
class it belongs to the Bro compiler will make it returnthis
making it possible to chain setter method calls.
The C struct timeval
struct timeval {
time_t tv_sec; /* seconds since Jan. 1, 1970 */
suseconds_t tv_usec; /* and microseconds */
};
can be bound like this using Bro in RoboVM:
public class Timeval extends Struct<Timeval> {
@StructMember(0) public native int tv_sec();
@StructMember(0) public native Timeval tv_sec(int i);
@StructMember(1) public native int tv_usec();
@StructMember(1) public native Timeval tv_usec(int i);
}
And used like this to call gettimeofday()
:
@Bridge static native int gettimeofday(Timeval tp, VoidPtr tzp);
public static void main(String[] args) {
Timeval t = new Timeval();
gettimeofday(t, null);
System.out.format("Seconds since epoch: %d\n", t.tv_sec());
}
The size in bytes of a Struct
can easily be queried by calling the static sizeOf()
method on the Struct
sub-class:
public class CGRect extends Struct<CGRect> { ... }
System.out.format("sizeof(CGRect) = %d\n", CGRect.sizeOf());
The @ByRef
and @ByVal
annotations can be used to control how a Struct
object is returned from a method or passed as a parameter to a method. @ByRef
means pass as pointer and is the default. @ByVal
means pass by value. The default can be changed to @ByVal
for a particular Struct
class by annotating the class with @ByVal
:
@ByVal
public class Person extends Struct<Person> { ... }
Struct
classes can contain other Struct
objects as members, either by value or by reference (i.e. by pointer). The default is @ByRef
with the same possibilities to override the default as for methods.
Below is an example of how the CocoaTouch struct CGRect
type is mapped to Java in the RoboVM Cocoa Touch bindings. A CGRect
has two members: the origin
is a CGPoint
struct
and the size
is a CGSize
struct.
struct CGRect {
CGPoint origin;
CGSize size;
};
public class CGRect extends Struct<CGRect> {
public CGRect() {}
public CGRect(float x, float y, float width, float height) {
origin().x(x).y(y);
size().width(width).height(height);
}
public CGRect(CGPoint origin, CGSize size) {
origin(origin);
size(size);
}
@StructMember(0) public native @ByVal CGPoint origin();
@StructMember(0) public native CGRect origin(@ByVal CGPoint origin);
@StructMember(1) public native @ByVal CGSize size();
@StructMember(1) public native CGRect size(@ByVal CGSize size);
}
A C union
is bound just like a C struct
but has overlapping @StructMember
indexes:
union TestUnion {
int i;
short s1;
short s2;
};
public class TestUnion extends Struct<TestUnion> {
@StructMember(0) public native int i();
@StructMember(0) public native TestUnion i(int i);
@StructMember(0) public native short s1();
@StructMember(0) public native TestUnion s1(short s1);
@StructMember(0) public native short s2();
@StructMember(0) public native TestUnion s2(short s2);
}
Bro provides the @Array
annotation which is used to bind array struct members. The @Array
annotation specifies the dimensions of the member's type. Space large enough to hold a native array of the specified type and dimensions will be reserved inside the struct.
Single-dimensional array:
struct Vector {
int values[3];
};
public class Vector extends Struct<Vector> {
@StructMember(0)
public native @Array(3) int[] values();
@StructMember(0)
public native Vector values(@Array(3) int[] values);
}
Multi-dimensional array:
struct Matrix {
int values[1, 2, 3];
};
public class Matrix extends Struct<Matrix> {
@StructMember(0)
public native @Array({1, 2, 3}) int[][][] values();
@StructMember(0)
public native Matrix values(@Array({1, 2, 3}) int[][][] values);
}
NOTE: The native data will be copied to and from the
int[]
andint[][][]
arrays in these examples. Changes in the Java arrays will not be directly reflected in the native data. The setter has to be called to update the native data.
An alternative to using Java arrays is to use a sub-class of java.nio.Buffer
instead:
Single-dimensional array:
struct Vector {
int values[3];
};
public class Vector extends Struct<Vector> {
@StructMember(0)
public native @Array(3) IntBuffer values();
@StructMember(0)
public native Vector values(@Array(3) IntBuffer values);
}
Multi-dimensional array:
struct Matrix {
int values[1, 2, 3];
};
public class Matrix extends Struct<Matrix> {
@StructMember(0)
public native @Array({1, 2, 3}) IntBuffer values();
@StructMember(0)
public native Matrix values(@Array({1, 2, 3}) IntBuffer values);
}
NOTE: The buffer's
capacity()
will be restricted to the dimension of the array. For multi-dimensional arrays this is the product of the dimensions, 123=6 for theMatrix
example.
A third option is to use one of the pointer classes in the org.robovm.rt.bro.ptr
package:
Single-dimensional array:
struct Vector {
int values[3];
};
public class Vector extends Struct<Vector> {
@StructMember(0)
public native @Array(3) IntPtr values();
@StructMember(0)
public native Vector values(@Array(3) IntPtr values);
}
Multi-dimensional array:
struct Matrix {
int values[1, 2, 3];
};
public class Matrix extends Struct<Matrix> {
@StructMember(0)
public native @Array({1, 2, 3}) IntPtr values();
@StructMember(0)
public native Matrix values(@Array({1, 2, 3}) IntPtr values);
}
It's also possible to have arrays of structs in a Struct
:
struct Color {
char r; char g; char b;
};
struct Gradient {
Color stops[3];
};
public class Color extends Struct<Color> { ... }
public class Gradient extends Struct<Gradient> {
@StructMember(0) public native @Array(3) Color[] stops();
@StructMember(0) public native Gradient stops(@Array(3) Color[] stops);
}
For unbounded native array members one should use one of the pointer classes in the org.robovm.rt.bro.ptr
package combined with the @ByVal
annotation:
struct PascalString {
int length;
char chars[];
};
public class PascalString extends Struct<PascalString> {
@StructMember(0) public native int length();
@StructMember(0) public native PascalString length(int length);
@StructMember(0) public native @ByVal BytePtr chars();
}
NOTE: There's no setter for the
chars
member as that would have required the length to be known at compile time. Setting the individual bytes ofchars
has to be done through theBytePtr
returned by the getter.
When creating an instance of a Struct
class Bro actually allocates two memory regions: one for the Java object and one for the actual struct data. The default is to allocate the struct data on the Java heap. There are two way to make sure that the underlying struct data is allocated on the GCed heap:
CGRect r = new CGRect();
CGRect r = Struct.allocate(CGRect.class);
This means that the data will be garbage collected when the garbage collector determines that the struct data isn't referenced from any other memory allocated on the garbage collected heap.
Allocating struct data on the GCed heap won't work for native code that holds on to a pointer to the struct data beyond a Java call into a native function since the native heap is not searched by the garbage collector. If the native side assumes ownership of the data and later frees it by a call to free()
the Java side has to use malloc()
to allocate it on the native heap:
CGRect r = Struct.malloc(CGRect.class);
CAUTION: If the native side holds on to a memory region allocated on the GCed heap care must be taken on the Java side to make sure that the memory isn't collected until the native side is done with it. As long as the Java
Struct
instance is referenced on the Java side the struct data can't be collected.
TIP: The memory region allocated to hold a struct's data will always be zeroed out regardless of whether using the Java heap or the native heap.
This allocates an array of 10 CGRect
instances:
public class CGRect extends Struct<CGRect> { ... }
CGRect l = Struct.allocate(CGRect.class, 10);
This allocates a contiguous memory region big enough to hold 10 CGRect
instances. The Struct
class defines a number of methods that can be used to iterate over these, e.g. next()
and previous()
.
Struct
also implements the Iterable
interface:
for (CGRect r : l) {
...
if (<some condition>) {
break;
}
}
CAUTION: The
Iterator
used whenfor
-looping like this is unbounded so abreak
is required to finish the loop.
The BytePtr
class and the other pointer classes in org.robovm.rt.bro.ptr
are in fact Struct
classes so to allocate a native array of 100 int
values one could do:
IntPtr myInts = Struct.allocate(IntPtr.class, 100);
Simple C enum
constants are mapped using Java Enum
types which implement the bro ValuedEnum
interface. Here's an example:
enum {
NSTextAlignmentLeft,
NSTextAlignmentCenter,
NSTextAlignmentRight,
NSTextAlignmentJustified,
NSTextAlignmentNatural
};
public enum NSTextAlignment implements ValuedEnum {
Left(0), Center(1), Right(2),
Justified(3), Natural(4);
private final long n;
private NSTextAlignment(long n) { this.n = n; }
public long value() { return n; }
public static NSTextAlignment valueOf(long n) {
for (NSTextAlignment v : values()) {
if (v.n == n) {
return v;
}
}
throw new IllegalArgumentException(
"No constant with value " + n + " found in "
+ NSTextAlignment.class.getName());
}
}
By default Bro marshals a ValuedEnum
as a signed 32-bit value. The default can be changed by specifying an explicit @Marshaler
on the enum type. Here's how to marshal NSTextAlignment
values as platform dependent (32-bit on 32-bit platforms, 64-bit on 64-bit platforms) signed integer values:
@Marshaler(ValuedEnum.AsMachineSizedSIntMarshaler.class)
public enum NSTextAlignment implements ValuedEnum {
...
}
There are ValuedEnum
marshalers for marshaling signed and unsigned 8-, 16-, 32- and 64-bit integer values. They are all available as inner classes in the ValuedEnum
interface.
TIP: The
@Marshaler
annotation can also be placed on a particular method return type or parameter type to only change the marshaler for that specific value.
Bro provides a class called Bits
that can be used to bind bitmask constants:
enum {
UIPopoverArrowDirectionUp = 1UL << 0,
UIPopoverArrowDirectionDown = 1UL << 1,
UIPopoverArrowDirectionLeft = 1UL << 2,
UIPopoverArrowDirectionRight = 1UL << 3,
...
};
public final class UIPopoverArrowDirection
extends Bits<UIPopoverArrowDirection> {
public static final UIPopoverArrowDirection None =
new UIPopoverArrowDirection(0L);
public static final UIPopoverArrowDirection Up =
new UIPopoverArrowDirection(1L);
public static final UIPopoverArrowDirection Down =
new UIPopoverArrowDirection(2L);
public static final UIPopoverArrowDirection Left =
new UIPopoverArrowDirection(4L);
public static final UIPopoverArrowDirection Right =
new UIPopoverArrowDirection(8L);
...
private static final UIPopoverArrowDirection[] values =
_values(UIPopoverArrowDirection.class);
public UIPopoverArrowDirection(long value) { super(value); }
private UIPopoverArrowDirection(long value, long mask) {
super(value, mask);
}
protected UIPopoverArrowDirection wrap(long value, long mask) {
return new UIPopoverArrowDirection(value, mask);
}
protected UIPopoverArrowDirection[] _values() {
return values;
}
public static UIPopoverArrowDirection[] values() {
return values.clone();
}
}
Bits
values can be ORed using the Bits.with(...)
methods in a manner very similar to how java.util.EnumSet.of(...)
works:
int upDown = UIPopoverArrowDirectionUp | UIPopoverArrowDirectionDown;
UIPopoverArrowDirection upDown = UIPopoverArrowDirection.with(
UIPopoverArrowDirection.Up, UIPopoverArrowDirection.Down);
TIP:
Bits
types is the preferred way to bind bitmasks since it provides some degree of type-safety and an easier to understand API. It does however impose a performance penalty compared to usingpublic static final
constants.
By default Bro marshals a Bits
instance as an unsigned 32-bit value. The default can be changed by specifying an explicit @Marshaler
on the class. Here's how to marshal UIPopoverArrowDirection
values as platform dependent (32-bit on 32-bit platforms, 64-bit on 64-bit platforms) unsigned integer values:
@Marshaler(Bits.AsMachineSizedIntMarshaler.class)
public final class UIPopoverArrowDirection
extends Bits<UIPopoverArrowDirection> {
...
}
There are Bits
marshalers for marshaling unsigned 8-, 16-, 32- and 64-bit integer values. They are all available as inner classes in the Bits
interface.
TIP: The
@Marshaler
annotation can also be placed on a particular method return type or parameter type to only change the marshaler for that specific value.
Type marshalers are used by Bro to convert from native types into Java types and vice versa.
Pointer marshalers marshal pointers to native objects to/from some Java object that wraps that pointer. The general contract for a pointer marshaler class looks like this:
public class MyTypeMarshaler {
@MarshalsPointer
public static MyType toObject(Class<?> cls, long handle, long flags) {
...
}
@MarshalsPointer
public static long toNative(MyType o, long flags) {
...
}
}
This marshaler marshals MyType
instances to/from native pointers (handle
). Bro uses the signatures of the @MarshalsPointer
methods to determine whether it is a method which marshals Java->native or native->Java. The Java type it can handle is determined by the signature (MyType
in this case).
The cls
parameter specifies the actual Class
used at the marshaling site. The passed in Class
is assignment compatible with the MyType
class or interface.
The flags
parameter gives some information on the call site. The possible values are defined by MarshalerFlags
class.
TIP: Marshaler method names are unimportant, the signatures are what matters.
- Developer Guide
- Changelog
- What we're currently working on and where you can help
- F.A.Q.
- Are non libGDX iOS apps supported?
- Is there a debugger available?
- How are bindings to existing native frameworks generated?
- Who is behind this project?
- Are there any alternatives to RoboVM?
- Can RoboVM be built on Linux?
- My app is crashing without a stacktrace, what should I do?
- Is XCode interface builder supported?
- Memory Management Guide
- Howto Create an Asset Catalog for XCode 9 Appstore Submission?
- Generate Admob Bindings with bro gen Tutorial