: code tutorial (v3)

Lesson 7: Calling Methods

Introduction

Calling methods is as easy as accessing fields, which we learned about in the previous lesson.

The proxy function names are as close to the Java names as possible. It is not always possible to have exactly the same name because certain names clash with C++ keywords like delete or with commonly used preprocessor definitions like min and max. Java also allows the declaration of methods with the same name as the class, even though they are not constructors. C++ does not allow that. In all these cases the proxy methods will (most likely) have an underscore appended to their Java name (though the exact details are configurable in the JunC++ion code generator).

Passing Arguments

The most important thing to understand is that proxy type instances are passed by reference and primitive arguments are passed by value. The use of reference type arguments allows the polymorphic passing of arguments, i.e. instances of derived types can be passed in place of the declared type, for example a String or a Hashtable can be passed through an Object reference.

C++ will also perform any legal conversions on the passed arguments so you can for example pass a C string literal in any place where a Java string is expected. null can be passed in any place that expects a non-primitive argument. The snippet below shows several Hashtable method invocations.

Hashtable   ht( _use_java_ctor );
String      key = "foo";
Object      value( _use_java_ctor );

// put a key/value pair in the hashtable
// the key (String) is passed polymorphically through an Object reference
ht.put( key, value );

// put another key/value pair in the hashtable
// both key and value are created through conversion constructors
ht.put( "FOO", "BAR" );

Returning Results

In Java, what you regard as an object is actually always a reference to an object. All non-primitive method results are therefore implicitly returned by reference. When a Java method is declared to return a java.lang.Object it actually rarely means that you will get back an instance of type java.lang.Object. That declaration just establishes that you will get back an object of a type that is assignable to java.lang.Object.

Please see the following Java code snippet where we use our expert knowledge about the type of the stored value even though the get() function is declared to return Object:

Hashtable ht = new Hashtable();
ht.put( "key", "value" );
String  result = (String)ht.get("key");

This works because the result is returned by reference, allowing a String to be passed in place of an Object.

C++ proxy methods on the other hand always return results by value. This is necessary to ensure that lifecycle management works even when the caller ignores the result. It also means that the function does not have to perform a potentially very expensive search for the best proxy type to use for the returned object. We leave it to the caller to reinterpret the returned object as a differently typed object (more on that in the lesson on casting).

Return by value vs. return by reference is a very important difference. The proxy method corresponding with the above Java method will be declared to return an instance of java::lang::Object, and that is exactly what it has to do: it has to return an actual instance of type java::lang::Object. Even if the Java method actually returns a String, the C++ call will return a java::lang::Object. The polymorphism is maintained purely via the reference to the Java object.

Your returned value of type java::lang::Object will internally maintain a reference to a Java Object that is assignable to java.lang.Object or null.

You should always check return values for null before you use them in a way that expects a valid Java object reference.

Let's take a look at the C++ code snippet that corresponds to the Java code above.

Hashtable ht( _use_java_ctor );
ht.put( "key", "value" );
String  result = String::dyna_cast(ht.get("key"));

As the get() function returns an actual java::lang::Object we cannot directly cast it to a java::lang::String. But we know that the Java object inside the result is a String, so we can use our special dyna_cast() function to reinterpret the result. Basically, this takes the Java object reference owned by the returned java::lang::Object, performs a type check on it and, if the type is compatible to what we're trying to reinterpret to, returns a proxy instance of that type.

The key take-away from this discussion should be that you cannot simply cast results to a different type, you have to use our special dyna_cast() function which really creates a new C++ object that holds the same Java object reference as the value it was given.

Exceptions

Most of the operations you can perform in Java can result in an exception or an error being thrown. We're introducing the topic of exceptions here even though it also technically applies to accessing fields and invoking constructors. We're discussing the topic in greater detail in its own lesson but we quickly want to touch on it here.

The runtime library will check all Java calls to see whether an exception was thrown on the Java side. If so, it will look for the C++ proxy exception type that matches the Java exception type most closely and throw an instance of that type into C++. This means that your exact C++ exception type depends not just on the Java exception type but also on the complete set of C++ exception types that are available.

Catching exceptions by reference allows you to catch exception types polymorphically, something that is highly advised so your code does not break if, after having written your code, you somehow gain access to a new proxy library that contains additional exception types and now the runtime will throw an exception of a different type than what you are handling.