|
From: Joe M. <jr...@cc...> - 2004-11-04 17:03:34
|
Ken Anderson <kan...@bb...> writes:
> Joe,
> Your talk made me want to look at code i haven't looked at for
> a long while - how Java methods are invoked. They are like Common
> Lisp generic functions, the right method is looked up at runtime
> based on the types of all aguments. There is not "type widening".
> There is no MOP, like you may have.
>
> For a Java class i was teaching i put all constructor/method/field
> invocation into a class Invoke, which was easily separated from
> JScheme, though we didn't advertise that.
>
> Basically an application of an instance method does a cache lookup
> on (list isStatic methodName ClassName canAccessPrivateData) The
> comments below shows the calling sequence for a JavaMethod. We do
> nothing fancy for method lookup except if there is only one, a
> common case, we invoke it and hope for the best.
>
> Instance methods do a lookup on isStatic, and methodName which we
> should memoize in the JavaMethod. It looks like we should
> specialize JavaMethod into JavaStaticMethod JavaInstanceMethod and
> JavaSpecifiedMethod (where both the class and name are specified) .
Wow. It looks as if our approaches are very different.
Larceny (the Scheme runtime) provides a `syscall' primitive that
simply transfers control to a known C# method. The Larceny runtime
leaves the arguments to syscall in known `registers' and expects the
Result register to be updated before control is returned.
Larceny also provides a primitive type called a ForeignBox. It has no
functionality whatsoever beyond being a first-class Scheme object, but
it is a useful wrapper for holding reflection objects.
So there is no attempt at all to integrate the .NET system with the
Scheme system at the primitive implementation level. Everything is
done at arms length and mediated via wrappers and `syscalls'.
The syscall for method invocation expects to find three Scheme objects
in the `registers': a foreign box holding the reflected method, a
foreign box holding the instance, and an array of foreign boxes
holding the arguments. It simply unboxes everything and attempts to
invoke the contents of the first box on the contents of the remainging
boxes:
// Get the arguments
SObject arg1 = Reg.register3;
SObject arg2 = Reg.register4;
SObject arg3 = Reg.register5;
// Unbox the method
MethodInfo mi = (MethodInfo) ((ForeignBox)arg1).value;
// Unbox the arguments
SObject[] sargv = ((SVL)arg3).elements;
object[] args = new object[sargv.Length];
for (int i = 0; i < args.Length; i++)
args [i] = ((ForeignBox)(sargv[i])).value;
// Call the method
object result = mi.Invoke (mi.IsStatic ? null : ((ForeignBox)arg2).value, args);
// Box up the result
Reg.Result = Factory.makeForeignBox (result);
return;
There are two ways to get a handle to a reflected method. There is a
syscall that can return a reflected method given a reflected type and
the types of the arguments, or one could invoke the GetMembers method
on the type and get an array of all the methods, fields, properties,
and constructors associated with the type. I use the former mechanism
only for bootstrapping (maybe a dozen or so methods).
It is in Scheme that everything interesting happens. I adapted an
object system based on Tiny-CLOS to Larceny (it started as Tiny-CLOS,
Eli Barzilay adapted it to PLT Scheme and enhanced it to make
`Swindle'. I adapted it and tailored it for Larceny and .NET) .NET
methods get wrapped within CLOS methods which are installed in the
appropriate CLOS generic functions. .NET type descriptors are wrapped
in CLOS class objects, and .NET objects are wrapped in CLOS
instances (hence the need for a MOP: a .NET type must be both an
instance and a class).
Suppose that we had invoked a .NET method (through my code) and it
returned a .NET object (in a ForeignBox). My code determines the
runtime type of the object in the box (via another syscall) and finds
the CLOS class object that represents that type. It instantiates an
instance of the class with a slot holding the ForeignBox. The
instance is what the user sees.
All .NET methods are instances of classes that inherit from the .NET
type `System.MethodBase'. My code knows about this and has
special-cased this particular class to inherit from both its reflected
meta-type *and* the Tiny-CLOS method class. Thus wrappers for
reflected methods are themselves CLOS methods.
When instantiating a wrapper for a reflected method, we need to
compute the specializers (to determine under what conditions it is
called) and a procedure (to determine what happens when it is called).
The specializers are determined by taking the declared types of the
method arguments and finding an appropriate reflected class object.
For the most part, this is simply the class object that represents the
reflected type, but some classes need special treatment. Therefore,
the generic function `argument-specializer' (which is the identity
function by default) is overridden in some particular cases. For
example, the specializer for the .NET string class is the Scheme
string class and the specializer for the .NET System.Object (the root
class) is instead the class of all Scheme objects. Arrays and enums
are handled specially here, too.
The procedure that gets invoked has to package things up for the
invoke syscall. .NET objects will already be in their foreign boxes,
but the boxes themselves need to be unwrapped from the CLOS
instances. Boxes holding integers and strings will need to be
constructed. So there is a generic function `argument-marshaler' that
given a reflected .NET type, returns a function that can correctly
convert a scheme object to a foreign box.
In addition, if the .NET method has optional arguments, the CLOS
method must supply the defaults which are specified in the reflected
method parameter list. The end result, in all its gory detail, is
this:
(define (clr-method-info->method class info)
(call-with-values
(lambda () (clr-methodbase/get-parameters info))
(lambda (required-parameter-count
optional-parameter-count
default-values
specializers
out-marshalers)
(let* ((declaring-type (clr-memberinfo/declaring-type info))
(name (clr-memberinfo/name info))
(instance-marshaler (argument-marshaler declaring-type))
(arity (if (= optional-parameter-count 0)
(+ required-parameter-count 1)
(make-arity-at-least (+ required-parameter-count 1))))
(max-arity (+ optional-parameter-count required-parameter-count 2))
(in-marshaler (return-marshaler (clr-methodinfo/return-type info))))
(make class
:arity arity
:max-arity max-arity
:clr-handle info
:name name
:specializers (cons (argument-specializer declaring-type) specializers)
:procedure (nary->fixed-arity
(lambda (call-next-method instance . args)
(dotnet-message 4 "Invoking method" name)
(in-marshaler
(clr/%invoke info
(instance-marshaler instance)
(marshal-out (+ optional-parameter-count required-parameter-count)
out-marshalers args default-values))))
(arity-plus arity 1)))))))
So if a method object is somehow returned from the .NET code to
Scheme, a CLOS instance that multiply inherits from both its
appropriate .NET class and the CLOS method class is constructed with
the appropriate specializers, marshalers, and defaults.
There is one more thing to do. When this method object is
instantiated, it should be registered with the appropriate generic
function to invoke it. There is a post-initialization method that is
added to subclasses of the System.MethodBase reflected class. This
registers the method:
(add-method initialize-instance
(make (*default-method-class*)
:arity 2
:specializers (list methodbase-class)
:procedure (lambda (call-next-method instance initargs)
(call-next-method)
(process-method
instance
(clr-methodbase/is-public?
(clr-object/clr-handle instance))))))
(define (process-method clr-info public?)
(let ((handle (clr-object/clr-handle clr-info)))
(if (clr-methodbase/is-static? handle)
(install-static-method (make-static-name handle) clr-info public?)
(install-instance-method (clr-memberinfo/name handle) clr-info public?))))
Since all this is part of the instance creation protocol, the only
thing that needs to be done at this point is to get the syscall layer
to return method objects. To bootstrap the system, I just walk the
type tree and ask it to list the methods.
Here's an example of method invocation. Suppose I call the .ToString
method on an object.
(.ToString foo)
The javadot syntax is handled by the macro expander, so this becomes
((clr/find-generic #f 'tostring) foo)
The #f indicates public method rather than private. The
*clr-public-generics* hash table will be searched to find the CLOS
generic function named 'tostring.
The CLOS generic function does the usual multimethod dispatch on the
types of its arguments (using the standard CLOS cacheing tricks to
make it fast). Supposing that foo were an instance of a CLOS wrapper
to a .NET object, the generic function would find that method
specialized to the class of (the wrapper to) foo. That method would
now be invoked. Recall that it will be something like this:
(lambda (instance . args)
(in-marshaler
(clr/%invoke info
(instance-marshaler instance)
(marshal-out (+ optional-parameter-count required-parameter-count)
out-marshalers args
default-values))))
But since there are no arguments beyond the instance, we can ignore
them and the effect is more like this:
(lambda (instance)
(in-marshaler
(clr/%invoke info
(instance-marshaler instance)
))
Info is the ForeignBox containing the reflected method and
instance-marshaler will simply be a function that unwraps the CLOS
wrapper. clr/%invoke is the syscall, so we end up passing the
appropriate ForeignBoxes to the C# code above.
The in-marshaler is a function that performs an appropriate action on
the return value. In this case, the return type of the .NET method is
System.String, so the return value of .ToString will be a ForeignBox
containing a .NET string object. The in-marshaler for most .NET
objects is to simply wrap them with the appropriate CLOS wrapper, but
we special case the in-marshaler for .NET strings to convert the
ForeignBoxed .NET string into a Scheme string.
--------------------
I'm sure your eyes have glazed over by now, so I'll finish here. It
looks as if we are both taking the same information into account when
trying to go from a Scheme generic to a Java or .NET method. It'd be
interesting to compare the utility, performance and the edge cases.
At this point, I'm the sole user of my code, so I really don't know
what end users will perceive as advantages or disadvantages to my
approach. From what I can tell, the overhead of tiny-CLOS is minimal
compared to the other overheads of going across the syscall boundary
and the interpreter overhead. In the simple, usual case, where there
is one method to invoke, we'll get the same results, but I wonder if
there are more complex cases where your method and mine diverge.
~jrm
|