I've added experimental support for using value types and reference arguments in
Java. The challenge is to do this without having to build a new Java compiler.
Here is a value type I constructed in C# for testing purposes:
public struct ValType
{
public int foo;
public ValType(int f)
{
foo
= f;
}
public override string ToString()
{
return "ValType:"
+ foo;
}
public static void Increment(ref ValType
v) {
v.foo++;
}
}
Here is a Java example that uses value types and calls a method with a reference argument:
import system.DateTime;
class ValTest
{
static DateTime
dt;
public static void main(String[]
args) {
ValType
v = new ValType(0);
v.foo
= 42;
System.out.println(v);
ValType[]
av = new ValType[1];
av[0]
= v;
ValType.Increment(av);
System.out.println(av[0]);
dt
= DateTime.get_Now();
DateTime
d = new DateTime(0);
d
= AddDay(d);
System.out.println(d);
System.out.println(dt);
}
private static DateTime
AddDay(DateTime dt) {
return dt.AddDays(1);
}
}
How is this code compiled? Whenever a value type is loaded from a field or array,
instantiated or returned from a method it is boxed. Local variables are references
to boxed instances of the value type. When a boxed value type instance is assigned
to a field, stored in an array or passed to a method, it is unboxed. Referenced method
arguments are exported by NetExp as arrays.
Let's look at a few examples:
ValType
v = new ValType(0);
Is compiled as:
ldc.i4.0
newobj instance
void ValType::.ctor(int32)
box ValType
stloc.0
The local variable v is of type object and
contains a reference to the boxed value.
Assigning a field in the boxed value:
ldloc.0
ldc.i4.s 42
stloc.1
unbox ValType
ldloc.1
stfld int32
ValType::foo
After loading the reference to the boxed value, the constant (42) gets stored
in a temporary local and then reference gets unboxed, this leaves a managed pointer
to the interior of the boxed value on the stack and the stfld instruction
uses this pointer to set the field.
Setting an array element:
ldloc.2
ldc.i4.0
ldloc.0
stloc.3
ldelema ValType
ldloc.3
unbox ValType
ldobj ValType
stobj ValType
The address of element 0 of the array is loaded, v is unboxed and copied into the
array.
Calling a method with a reference argument:
ldloc.2
stloc.s V_4
ldloc.s V_4
ldc.i4.0
ldelema ValType
call
void ValType::Increment(valuetype ValType&)
The Java code passes an array and the byte code to CIL compiler takes the address
of the first element of the array and passes that to the method. BTW, if the array is
of length zero, ldelema throws
a System.IndexOutOfRangeException.
Pros and Cons of this Approach
The advantage of enabling value type of reference arguments are that it is easier
to leverage .NET framework code from within Java code (targetted at IKVM.NET) and
it enables me to write more glue code in Java.
The downside is the non-intuitive behavior. Compare these two lines:
v.foo
= 42;
av[0].foo = 42;
The first line does what you'd expect, it assigns 42 to the field foo of the boxed
value type. However, the second line has no effect, because it results in a boxed
copy being loaded from the array, that boxed copy's field foo is assigned the value
42, but it is not copied back into the array. This also applies to fields, only local
variables contain boxed value types and can be manipulated safely.
Finally, a small problem with arrays of value types is that the Java compiler will
happily compile:
Object[]
array = new ValueType[1];
Obviously this isn't legal.
I'd like to get feedback from people who either like or dislike this new
functionality. Of course, any ideas for alternative approaches would be most welcome
too.