SISC can be used as a scripting language for Java, or Java may be used to provide functionality to Scheme. Such activity is collectively termed 'bridging'. In SISC bridging is accomplished by a Java API for executing Scheme code and evaluating Scheme expressions, and a module that provides Scheme-level access to Java objects and implementation of Java interfaces in Scheme.
Calling programmer-defined Scheme code from Java is a simple process, but requires some initialization before proceeding. Depending on the use case of SISC, the initialization may be largely automatic. Any Scheme call first requires four resources:
An Application Context (AppContext),
which encapsulates the entirety of a single application's
interactions with SISC. This is initialized with ...
A Heap, which provides the environments and base code to execute programs using ...
A Dynamic Environment, which contains thread specific values such as the current I/O ports, and
An Interpreter, which provides
the engine and API for actually evaluating Scheme code.
Each resource is described in the sections below. The gateway
for interacting with these resources is primarily the
sisc.interpreter.Context utility class.
The Application Context, represented by the class
sisc.interpreter.AppContext, holds
references to the top-level environment, global configuration
properties, and other resources which are unique to a
single usage of SISC. This should not be confused
with multiple threads or invocations into the same
application.
An AppContext is created simply using the default constructor,
or a constructor which takes a Java
Properties class with overrides
for global Scheme properties as described throughout
this manual.
The application context is the token used
by most of the API for calling Scheme from Java code,
though many of the API calls can either infer the
AppContext from the currently
executing thread, or defer to a default context.
A default application context may be necessary when Scheme code
is called through callbacks or other Java mechanisms in
code which is not aware of SISC or the Scheme code
which is being called. The default application context
can be set explicitly after creating the context,
but is also set implicitly by SISC if there is not
already a default context when one is needed.
To set the default application context, use the following
method from Context:
public void setDefaultAppContext(sisc.interpreter.AppContext ctx);
Set the default
AppContextto the instance provided.
public sisc.interpreter.AppContext getDefaultAppContext();
Retrieves the current default
AppContext, possibly creating and initializing one using the default heap if no default was set.
Remember that an application context is nearly always useless until initialized with a heap, as described in the next section.
The heap is a serialization of all the pre-compiled code which makes up both the base Scheme language provided by SISC, and many of its libraries. Distributions of SISC come with a prepackaged heap which is sufficient for most usages, and customizing a heap should be viewed as a last resort, as precompiled libraries usually solve the same problem.
A heap is a randomly accessed structure which must be loaded
into an application context. The heap can be automatically
located if it is in the directory pointed to by the
SISC_HOME environment variable, the
current directory, or on the Java classpath. In the last case
however, it will be loaded into memory entirely, rather than
read in as needed. The automatic heap location involves
calling addDefaultHeap on the
AppContext, and is used
when an application context is created implicitly.
Uses
findHeap,openHeap, andaddHeapto find and register a default heap with this application context
The URL that is produced through this discovery can be
obtained using the findHeap method in
AppContext:
public static java.net.URL findHeap(String heapName);Searches for the heap with the given name, or if null,
sisc.shpin several candidate locations. If a heap is found, a URL pointing to it is returned, otherwise null is returned.
One of the locations searched is the classpath, by searching for a
heap anchor, and loading the heap file from the
same package. To do this, the heap must be on the classpath in the
same location as HeapAnchor.class. A utility
script, build-heapjar.scm,
executed as an SRFI-22 script, is included in the
scheme-src directory of the full distribution. It is
invoked from the directory containing the heap, and will by default
emit sisc-heap.jar into the current directory.
If that jar file is on the classpath, findHeap
will locate it automatically.
Once located, the heap is opened and registered with the
AppContext, allowing the context
to then be used for Scheme evaluation. This is done
with the openHeap and addHeap
methods:
The dynamic environment, represented by the
sisc.env.DynamicEnvironment class,
is the datastructure which stores dynamic
variables. That is, variables whose values are not
scoped lexically and are associated with threads rather than
code. This includes such values as the current input and
output ports, Scheme parameters, and the class path.
Each interpreter contains a reference to its current dynamic environment, and the dynamic environment cannot be shared between two threads, or by more than one application context called in the same thread. They should be shared across call boundaries in a single thread and single application, but must be created anew for external calls and cross application calls, and cloned for new threads.
It should seem obvious, then, that maintaining the correct
dynamic environment in all call situations can be tricky.
Fortunately, the supported API calls in
Context detect and do the Right Thing
in most situations.
The Interpreter class contains the
engine for evaluating Scheme code, and API methods for
triggering that evaluation. Each thread of execution
must have its own Interpreter instance.
Interpreters are obtained before one or more calls into
Scheme, and using methods on the
Context helper class depending on
whether the call is an Internal or
External call.
A Scheme application can execute in multiple threads. Each thread must
have its own dynamic environment, containing entities such as the current
input and output ports. Dynamic environments are represented by
instances of the
sisc.env.DynamicEnvironment class.
Internal calls need to create fresh interpreters in order to preserve and subsequently restore the state of the surrounding Scheme execution. Thus a single thread may be home to several interpreters.
In any case, the programmer must ensure that all calls to an interpreter are made by the same thread. If another thread wishes to execute Scheme code, it must follow the API below to obtain a different interpreter.
An external call is a call made from Java to Scheme with no
preceding call from Scheme to Java, in other words, the call
is entirely external to the Scheme environment. For
example, this may occur as a result of a timer expiration or
a thread created by Java. In this case, the application
context must be specified, and a new dynamic environment
(containing thread specific information such as the current
input and output ports) must be created. The preferred
method for an external call is called a managed
external call, and uses the visitor pattern with
one of the following methods in the Context
class:
public static Object execute(sisc.interpreter.AppContext ctx,
sisc.interpreter.SchemeCaller caller)
throws sisc.interpreter.SchemeException;Creates an Interpreter context for the application context if provided, or the default application context if
ctxis null, and callsexecutein the caller with the new Interpreter. When the caller returns, the Interpreter is freed, and the return value of the caller is returned.
public static Object execute(sisc.interpreter.SchemeCaller caller)
throws sisc.interpreterSchemeException;Creates an Interpreter context for the default application context, loading the default heap if necessary, and calls the
executemethod of thecallerobject with the new Interpreter (that is, it is equivalent toexecute(null, caller)). This simple interface is the one you should use if you do not have a particular reason to use a non-default application context.
The visitor instances implement
sisc.interpreter.SchemeCaller
and are responsible for implementing the following method:
public Object execute(sisc.interpreter.Interpreter r)
throws sisc.interpreter.SchemeException;Utilizes the given Interpreter to make Java to Scheme calls. Any Object may be returned.
As an alternative to this managed external call, a
call from Java to Scheme can be made by using the
enter method of
Context to obtain an interpreter,
then making several calls to the interpreter, then releasing
it using exit. This has some
weaknesses, however. Because this pair of calls cannot enforce that
all intervening calls to the Interpreter run in the same thread,
subtle issues can arrise if the Interpreter context is saved as a reference
in a program and used by competing threads. Other, more subtle
issues exist, such as the association of a thread handle (as retrievable
using SRFI-18) in Scheme with the Java thread which is making the call.
For this reason, the managed external call form is preferred
whenever possible.
public sisc.interpreter.Interpreter enter(sisc.interpreter.AppContext ctx,
sisc.env.DynamicEnvironment ctx);Obtains an instance of Interpreter for the provided application context if provided, the current or default otherwise. If provided, the given dynamic environment is used rather than the environment selected automatically for the type of call.
The dynamic environment optional argument in the
enter method may be specified if
one wants to use a different mechanism for
finding applications and dynamic environments. For instance,
threads created from Scheme should probably execute within
the application that created them and using a dynamic
environment that is cloned from the dynamic environment
present when the thread is started. The
enter method can therefore be used as
general mechanism for obtaining a new interpreter that uses
a specific application and dynamic environment.
When the Interpreter is no longer by the current thread needed it must be released using:
Internal calls are calls to Scheme made from Java, from a previous call into Java from Scheme. In other words, the call to Scheme is made internally from other Scheme code. One can determine if a call is internal in the following manner:
Interpreter current = Context.currentInterpreter();
if (current == null) { ...make external call...}
else { ...make internal call... }
This case is more complex, as it requires maintaining the correct dynamic environment and Interpreter instance to preserve return context. When making an internal call, one typically wants to make the call in an interpreter that shares the same application context and dynamic enviornment as the calling interpreter.
Fortunately, the details are managed by the
Context helper class.
The same calling mechanisms are used as in an external call,
but the application context is ommited as a parameter to the
functions.
No matter which mechanism is used, the
Interpreter is eventually used to
call Scheme code, using any of the following methods:
public Value eval(String expr)
throws sisc.interpreter.SchemeException, java.io.IOException;Evaluates expressions read from a string, returning the result of the evaluation of the last expression.
public Value eval(InputStream stream,
String sourceId)
throws sisc.interpreter.SchemeException, java.io.IOException;Evaluates expressions read from an input stream, returning the result of the evaluation of the last expression. The
sourceIdidentifies the source of the stream for display purposes.
public Value eval(Value val)
throws sisc.interpreter.SchemeException;This is the same as calling
(evalval)in Scheme.
public Value eval(Procedure proc,
Value[] args)
throws sisc.interpreter.SchemeException;This is the same as calling
(procarg...)in Scheme. This is the most efficient type of call, as it requires no parsing or compilation.
Several such calls can be made consecutively on the same
interpreter. A SchemeException may
be thrown by any of these methods if an error occurs in the
Scheme code.
Interpreter.eval() throws a
sisc.interpreter.SchemeException when an evaluation
causes an exception that is not caught inside the
evaluation. When making internal calls the exception can be
propagated to the calling interpreter in one of four ways:
by throwing a RuntimeException
- this will be reported as"Error in
"prim-name:
description".
by calling
Module.throwPrimException("description")
- this will be
reported as "Error in
prim-name:
description".
by calling
throwNestedPrimException("description",
schemeException)
- this will be reported as "Error in
prim-name:
description\nnested-description".
by calling
throwNestedPrimException(schemeException)
- this will be reported as "Error in
prim-name:
exception during nested
call\nnested-description".
Scheme code can throw Java exceptions (see the section called “Exception Handling”). The
sisc.modules.s2j.Util class contains
a static method
javaException(schemeException)
that extracts the Java exception from a
SchemeException, or, if no Java exception
is present, returns the SchemeException.
Continuations do not cross the Scheme/Java boundary. In the embedded call scenario invoking a continuation inside the embedded call will not discard the computation of the caller. The embedded call will return when the continuation returns. If the continuation contains the read-eval-print loop the return will never happen. Similarly, capturing a continuation inside a call (embedded or external) will only capture the continuation to point where the call was made.
Capturing and invoking a continuation within the same call works correctly.
Scheme ports are thin wrappers around the Java I/O hierarchy, adding some functionality needed by Scheme. As such, it is trivial to obtain Java compatible I/O objects from Scheme ports. For information on obtaining Scheme and Java compatible I/O objects in Scheme, see the Java I/O module described in the section called “Java Ports”.
Scheme ports are encapsulated in the
SchemeBinaryInputPort,
SchemeBinaryOutputPort,
SchemeCharacterInputPort,
SchemeCharacterOutputPort classes
in the sisc.data package. An
instance of a port class contains an accessor which returns
the relevant Java I/O type, as described for each class below.
public sisc.data.SchemeBinaryInputPort {public java.io.InputStream getInputStream();
}Return a Java
InputStreamfor accessing this Scheme port.public sisc.data.SchemeBinaryOutputPort {public java.io.OutputStream getOutputStream();
}Return a Java
OutputStreamfor accessing this Scheme port.public sisc.data.SchemeCharacterInputPort {public java.io.Reader getReader();
}Return a Java
Readerfor accessing this Scheme port.public sisc.data.SchemeCharacterOutputPort {public java.io.Writer getWriter();
}Return a Java
Writerfor accessing this Scheme port.
The tables below cover the most common use cases for calling Scheme from Java, and provide simple pseudocode examples.
Table 8.1. Typical Java to Scheme, External Calls
| Situation | Code |
|---|---|
| Default Application Context | |
|
Heap can be located automatically... | No action required. |
|
...or, with a custom heap |
AppContext ctx=new AppContext();
SeekableInputStream myHeap=AppContext.openHeap(myHeapURL);
ctx.addHeap(myHeap);
Context.setDefaultAppContext(ctx);
|
Then, making the external call. |
Context.execute(mySchemeCaller);
|
| Custom Application Context | |
Creating the context. |
AppContext ctx=new AppContext(myProperties);
|
|
Heap can be located automatically ... |
ctx.addDefaultHeap();
|
|
... or, with a custom heap |
SeekableInputStream myHeap=AppContext.openHeap(myHeapURL);
ctx.addHeap(myHeap);
|
Then, making the external call. |
Context.execute(ctx, mySchemeCaller);
|
Table 8.2. Typical Java to Scheme, Internal Calls
| Situation | Code |
|---|---|
Making the internal call. |
Context.execute(mySchemeCaller);
|
Requires: (import s2j)
The High-Level S2J API allows Scheme code to instantiate Java classes, call methods on Java objects, access/modify fields of Java objects and implement Java interfaces that delegate to Scheme code.
Java classes are types in SISC's extensible type system (see the section called “Type System”). They are made accessible to Scheme code by one of the following procedures / special forms:
procedure:
(java-class symbol) => jclassReturns the Java class of name
symbol, which can also be the name of a primitive type or an array type.
(java-class '|java.lang.String|) ;=> <jclass> (define <java.io.object-input-string/get-field**> (java-class '|java.io.ObjectInputStream$GetField[][]|))
syntax:
(define-java-class scheme-name [java-name])Binds
scheme-nameto the Java class named byjava-name, or, if no such parameter is supplied, by the mangledscheme-name.
(define-java-class <jstring> |java.lang.String|) (define-java-class <java.io.object-input-string/get-field**>)
syntax:
(define-java-classes form ...)
where formis of the formscheme-nameor(scheme-namejava-name)Creates bindings for several Java classes.
The form expands into several
define-java-classforms.
(define-java-classes (<jstring> |java.lang.String|) <java.io.object-input-string/get-field**>)
Mangling of class names allows classes to be identified more
schemely,
e.g. <java.io.object-input-stream/get-field**>
corresponds to the Java type
java.io.ObjectInputStream.GetField[][]
(note that GetField is a nested class). More formally,
mangling of class names checks for the presence of angle
brackets (<>) around the
scheme-name. If they are present they
are stripped. All the identifiers between the dots
(.) are passed through field name mangling
(see the section called “Fields”). The character following
the last dot is upcased. A slash (/) is
treated as a nested class indicator; it is replaced with
dollar ($ and the character following it is
upcased. Trailing stars (*) characters are
replaced with pairs of brackets ([]).
There are predicates for determining whether a Scheme value is a Java class or interface. All Java interfaces are also Java classes.
procedure:
(java-class? value) => #t/#fReturns #t if
valueis a Java class, #f otherwise.
(java-class? (java-class '|java.lang.String|) ;=> #t (define <java-io-object-input-string/get-field**> (java-class '|java.io.ObjectInputStream$GetField[][]|)) (java-class? <java.io.object-input-string/get-field**>) ;=> #t
procedure:
(java-interface? value) => #t/#fReturns #t if
valueis a Java interface, #f otherwise.
(java-interface? (java-class '|java.util.Map|)) ;=> #t (java-interface? (java-class '|java.lang.String|) ;=> #f
Java classes are serializable by the SISC runtime.
Java methods are made accessible to Scheme code as procedures that can invoke any method of a given name on any Java object. Method selection is performed based on the types of the arguments to the procedure call. Static Java methods can be invoked by passing an instance of the appropriate class or an appropriately typed null object (see the section called “Instances”) as the first argument to the procedures.
procedure:
(generic-java-method symbol) => procedureReturns a procedure that when invoked with a Java object as the first argument and Java values as the remaining arguments, invokes the best matching named method named
symbolon the Java object and returns the result.
(generic-java-method '|getURL|) ;=> <jmethod> (define empty-list? (generic-java-method '|isEmptyList|))
syntax:
(define-generic-java-method scheme-name [java-name])Binds
scheme-nameto the generic Java method named byjava-name, or, if no such parameter is supplied, by the mangledscheme-name.
(define-generic-java-method get-url |getURL|) (define-generic-java-method empty-list?)
syntax:
(define-generic-java-methods form ...)
where formis of the formscheme-nameor(scheme-namejava-name)Creates bindings for several generic Java methods.
The form expands into several
define-generic-java-methodforms.
(define-generic-java-methods (get-url |getURL|) empty-list?)
Method name mangling allows methods to be identified more
schemely, e.g. empty-list? corresponds to
the Java method name
isEmptyList. More formally, mangling
of method names removes trailing exclamation marks
(!) and replaces trailing question marks
(?) with a leading
is-. The result of this mangling is passed
through field mangling (see the section called “Fields”).
Generic Java methods are serializable by the SISC runtime.
Fields are made accessible to Scheme code as procedures that can get / set any field of a given name on any Java object. If several fields of the same name are present in the object due to the object class's inheritance chain, the most specific field, i.e. the one bottommost in the inheritance hierarchy, is selected. Static Java fields can be accessed / modified by passing an instance of the appropriate class or an appropriately typed null object (see the section called “Instances”) as the first argument to the procedures.
Generic Java field accessors, i.e. procedures that allow Scheme code to obtain the value of a Java field, can be defined as follows:
procedure:
(generic-java-field-accessor symbol) => procedureReturns a procedure that when invoked with a Java object as the first argument, retrieves the value of the Java field named
symbolin the Java object.
(generic-java-field-accessor '|currentURL|) ;=> <jfield> (define :current-input-port (generic-java-field-accessor '|currentInputPort|))
syntax:
(define-generic-java-field-accessor scheme-name [java-name])Binds
scheme-nameto the generic Java field accessor for fields namedjava-name, or, if no such parameter is supplied, the mangledscheme-name.
(define-generic-java-field-accessor :current-url |currentURL|) (define-generic-java-field-accessor :current-input-port)
syntax:
(define-generic-java-field-accessors form ...)
where formis of the formscheme-nameor(scheme-namejava-name)Creates bindings for several generic Java field accessors.
The form expands into several
define-generic-java-field-accessorforms.
(define-generic-java-field-accessor (:current-url |currentURL|) :current-input-port)
Generic Java field modifiers, i.e. procedures that allow Scheme code to set the value of a Java field, can be defined as follows:
procedure:
(generic-java-field-modifier symbol) => procedureReturns a procedure that when invoked with a Java object as the first argument and a Java value as the second argument, sets the value of the Java field named
symbolin the Java object to that value.
(generic-java-field-modifier '|currentURL|) ;=> <jfield> (define :current-input-port! (generic-java-field-modifier '|currentInputPort|))
syntax:
(define-generic-java-field-modifier scheme-name [java-name])Binds
scheme-nameto the generic Java field modifier for fields namedjava-name, or, if no such parameter is supplied, the mangledscheme-name.
(define-generic-java-field-modifier :current-url! |currentURL|) (define-generic-java-field-modifier :current-input-port!)
syntax:
(define-generic-java-field-modifiers form ...)
where formis of the formscheme-nameor(scheme-namejava-name)Creates bindings for several generic Java field modifiers.
The form expands into several
define-generic-java-field-modifierforms.
(define-generic-java-field-modifier (:current-url! |currentURL|) :current-input-port!)
The mangling of field names allows fields to be identified
more schemely. By convention field accessors should be named
with a leading colon (:) followed by the
field name, and field modifiers with a leading colon
(:) followed by the field name followed by
an exclamation mark (!),
e.g. :foo-bar and
:foo-bar! are the names of the accessor and
modifier for Java fields named
fooBar. Mangling of field names upcases any
character following a dash (-) and removes
all characters that are not legal as part of Java
identifiers.
Generic Java field accessors and modifiers are serializable by the SISC runtime.
Scheme code can instantiate Java classes with a call to the following procedure:
procedure:
(java-new jclass jobject ...) => jobjectSelects a constructor of
jclassbased on the types of thejobjects and calls it, returning the newly created object.
(define-java-class <java.util.linked-hash-set>) (java-new <java.util.linked-hash-set> (->jint 100)) ;=> <jobject>
There is a predicate for determining whether a value is a Java object:
procedure:
(java-object? value) => #t/#fReturns #t if
valueis a Java object, #f otherwise.Note that, unlike in Java, instances of primitive Java types are considered to be Java objects.
(define-java-class <java.util.linked-hash-set>) (define hs (java-new <java.util.linked-hash-set> (->jint 100))) (java-object? hs) ;=> #t (java-object? (->jint 100)) ;=> #t
Unlike in Java, null objects are typed. Typed null objects play a key role in invoking static methods an accessing / modifying static fields.
procedure:
(java-null jclass) => jnullReturns a Java null object of type
jclass.
(define-java-class <java.util.linked-hash-set>) (java-null <java.util.linked-hash-set>) ;=> <jnull>
There is a predicate for determining whether a value is a Java null. All nulls are also Java objects.
procedure:
(java-null? value) => #t/#fReturns #t if
valueis a Java null object, #f otherwise.
(define-java-class <java.util.linked-hash-set>) (java-null? (java-null <java.util.linked-hash-set>)) ;=> #t
For
convenience, jnull is bound to the typed
null object obtained by calling java-null
on java.lang.Object.
Any invocation of a Java method, or access to a Java fields that returns a Java null does so typed based on the declared return / field type.
Comparison of Java objects using eqv?
compares the objects using Java's ==
comparison. equal?, on the other hand,
compares the objects using Java's equals
method. eq? uses pointer equality on the
Scheme objects representing the Java objects and is therefore
not generally useful. Applying eq?,
eqv? or equal? to a
mixture of Java objects and other Scheme values returns
#f.
Java objects are only serializable by the SISC runtime if they support Java serialization. Java nulls are always serializable.
Scheme code can create Java arrays with a call to the following procedure:
procedure:
(java-array-new jclass size) => jarrayCreates an array of component type
jclasswith dimensionssize, which can be a number (for a single-dimensional array), or a vector / list of numbers (for multi-dimensional arrays).
(java-array-new <jint> 2) ;=> <jarray> (define-java-class <java.lang.string>) (java-array-new <java.lang.string> '#(2 2 2)) ;=> <jarray>
There is a predicate for determining whether a value is a Java array. All Java arrays are also Java objects.
procedure:
(java-array? value) => #t/#fReturns #t if
valueis a Java array, #f otherwise.
(java-array? (java-array-new <jint> 2)) ;=> #t
Elements of arrays are accessed and modified with:
procedure:
(java-array-ref jarray index) => jobjectReturns the element at index
indexofjarray.indexcan be a number, for indexing into the first dimension of the array, or vector / list of numbers for multi-dimensional indexing.
(define a (->jarray (map ->jint (iota 10)) <jint>)) (java-array-ref a 1) ;=> <java int 1> (java-array-ref a '(1)) ;=> <java int 1>
procedure:
(java-array-set! jarray index jobject)Sets the element at index
indexofjarraytojobject.indexcan be a number, for indexing into the first dimension of the array, or vector / list of numbers for multi-dimensional indexing.
(define a (->jarray (map ->jint (iota 10)) <jint>)) (java-array-set! a 1 (->jint 2)) (java-array-ref a 1) ;=> <java int 2> (define a (java-array-new <jint> '#(2 2 2))) (java-array-set! a '#(1 1 1) (->jint 1))
The length of a Java array can be determined with
procedure:
(java-array-length jarray) => numberReturns the length of
jarray.
(define a (->jarray (map ->jint (iota 10)) <jint>)) (java-array-length a) ;=> 10 (define a (java-array-new <jint> '#(2 3 4))) (java-array-length a) ;=> 2
Scheme vectors and lists can be converted to Java array and vice versa.
procedure:
(->list jarray) => listCreates a list containing the elements of
jarray.
(define a (->jarray (map ->jint (iota 5)) <jint>)) (map ->number (->list a)) ;=> '(0 1 2 3 4)
procedure:
(->vector jarray) => vectorCreates a vector containing the elements of
jarray.
(define a (->jarray (map ->jint (iota 5)) <jint>)) (map ->number (vector->list (->vector a))) ;=> '(0 1 2 3 4)
procedure:
(->jarray list-or-vector jclass) => jarrayCreates a one-dimensional array of type
jclassand fills it with the values obtained from the Scheme vector or list.
(define a (->jarray (map ->jint (iota 5)) <jint>))
Scheme code cannot create sub-classes of existing Java classes. It is, however, possible to create classes implementing existing Java interfaces. These classes are called proxies. Calling a method on a proxy invokes a user-definable Scheme procedure, based on the name of the method, passing the proxy object and the parameters of the method invocation as arguments. The result of the invocation is returned as the result of the method call.
syntax:
(define-java-proxy signature interfaces method ...)
where signatureis of the form(nameparam...),interfacesis of the form(interface...), andmethodis of the form(definemethod-nameprocedure), or(define (method-namemethod-arg...) .body)Creates a proxy generator procedure and binds it to
name. A proxy class is created that implements all theinterfaces. When the generator is invoked, an instance of the proxy class is returned that delegates all method invocations to the Scheme procedures in the method definition list, based on the names of the methods.The first kind of definition form defines
procedureto be the method handler for the java method namedmethod-name.method-nameundergoes name mangling as described in the section called “Methods”. Note thatprocedureis inside the lexical scope of the generator procedure, soparams are accessible inside it.The second kind of definition form is equivalent to the following first-type form:
(definemethod-name(lambda (method-arg...) .body)).If a method is invoked on a proxy for which no method handler exists and error is returned to the caller.
(define-java-classes <java.util.comparator> <java.util.arrays> <java.lang.object>) (define-java-proxy (comparator fn) (<java.util.comparator>) (define (.compare this obj1 obj2) (let ([x (java-unwrap obj1)] [y (java-unwrap obj2)]) (->jint (cond [(fn x y) -1] [(fn y x) +1] [else 0]))))) (define-generic-java-method sort) (define-java-class <java.lang.object>) (define (list-sort fn l) (let ([a (->jarray (map java-wrap l) <java.lang.object>)]) (sort (java-null <java.util.arrays>) a (comparator fn)) (map java-unwrap (->list a)))) (list-sort < '(3 4 2 1)) ;=> '(1 2 3 4) (list-sort string<? '("foo" "bar" "baz")) ;=> '("bar" "baz" "foo")
For convenience, all the primitive Java types,
i.e. void,
boolean, double,
float, long,
int, short,
byte, char, are
predefined and bound to <jvoid>,
<jboolean>,
<jdouble>,
<jfloat>,
<jlong>,
<jint>,
<jshort>,
<jbyte>,
<jchar>, respectively.
When calling Java methods, invoking Java constructors, accessing or modifying Java fields, no automatic conversion is performed between ordinary Scheme values and Java values. Instead explicit conversion of arguments and results is required. Automatic conversion is not performed for the following reasons:
For some Scheme types, such as numbers, the mapping to
Java types is one-to-many, e.g. a Scheme number could be
converted to a byte,
short,
int, etc. This causes ambiguities
when automatic conversion of parameters is attempted.
Some Java types have several corresponding Scheme types, e.g. a Java array could be represented as Scheme list or vector - this causes ambiguities when automatic conversion of results is attempted.
Conversion carries an overhead that can be significant. For instance, Java strings have to be copied "by value" to Scheme strings since the former are immutable and the latter aren't. In a chained-call scenario, i.e. where the results of one method invocation are passed as arguments to another, the conversion is unnecessary and a wasted effort.
Conversion breaks the object identity relationship. In a chained-call scenario, the identities of the objects passed to the second call are different from the ones returned by the first. This causes problems if the called Java code relies on the object identity being preserved.
Conversion conflicts with generic procedures. The method selection mechanism employed by generic procedures relies on objects having exactly one type. Automatic conversion effectively gives objects more than one type - their original type and the type of the objects they can be converted to. While it would be technically possible to devise a method selection algorithm that accommodates this, the algorithm would impose a substantial overhead on generic procedure invocation and also make it significantly harder for users to predict which method will be selected when invoking a generic procedure with a particular set of arguments.
Conversion functions are provided for converting instances of primitive Java types to instances of standard Scheme types:
procedure:
(->boolean jboolean) => #t/#fprocedure:
(->character jchar) => characterprocedure:
(->number jbyte/jshort/jint/jlong/jfloat/jdouble) => number
Conversion functions also exists for the opposite direction, i.e. converting instances of standard Scheme types to instances of primitive Java types
procedure:
(->jboolean boolean) => jbooleanprocedure:
(->jchar character) => jcharprocedure:
(->jbyte number) => jbyteprocedure:
(->jshort number) => jshortprocedure:
(->jint number) => jintprocedure:
(->jlong number) => jlong
Finally, there are conversion functions for converting between Java strings and Scheme strings and symbols:
Scheme values are not Java objects and hence cannot be passed as arguments in Java method or constructor invocations or when setting Java fields. However, all Scheme values are internally represented by instances of classes in the SISC runtime. S2J provides a mechanism to get hold of this internal representation as an S2J Java object. The converse operation is also supported - a Java instance obtained via a Java method or constructor invocation or field access in S2J can be turned into a Scheme value if it is an instance of an appropriate SISC runtime class. These two operations are called "wrapping" and "unwrapping" respectively because conceptually the scheme object is wrapped to make it appear like a Java object and the wrapper is removed in order to recover the original Scheme object.
procedure:
(java-wrap value) => jobjectReturns the Java object that represents the Scheme
valuein SISC's runtime.
procedure:
(java-unwrap jobject) => valueReturns the Scheme value represented by the
jobject. Ifjobjectis not an object representing a Scheme value in SISC's runtime and error is thrown.
(define-java-class <java.lang.object>) (define a (java-array-new <java.lang.object> '#(1))) (java-array-set! a '#(0) (java-wrap 'foo)) (java-unwrap (java-array-ref a '#(0))) ;=> 'foo
Wrapping and unwrapping allows Scheme values to be used in generic (i.e. not type-specific) Java operations, such as those of the Java collection API. It is also frequently used in connection with proxies when Scheme objects are passed back and forth through layers of Java to a Scheme-implemented proxy that manipulates them. Finally, wrapping and unwrapping permit SISC Scheme code to interface to the SISC runtime.
In Java each object is a potential thread synchronization point. Therefore Scheme code needs to be able to synchronize on Java objects in order for it to interoperate properly with Java in a multi-threaded application. This is accomplished by the following procedure:
procedure:
(java-synchronized jobject thunk) => valueRuns
thunkin a block synchronized onjobject, returning the result returned bythunk. This is the equivalent tosynchronized (jobject) { returnthunk(); }in Java.It is illegal for
thunkto invoke continuations that escapethunk, or for code outsidethunkto invoke a continuation captured insidethunk.
(define-java-class <java.lang.object>) (define mtx (java-new <java.lang.object>)) (define v 0) (define (inc-v) (java-synchronized mtx (lambda () (set! v (+ v 1)) v))) (define (dec-v) (java-synchronized mtx (lambda () (set! v (- v 1)) v))) (import threading) (begin (parallel inc-v dec-v inc-v inc-v dec-v dec-v) v) ;=> 0
Java exceptions are propagated to scheme and can be caught
like any other exception, e.g. with
with/fc as defined in the section called “Failure Continuations”. The s2j
module exports augmented versions of the
print-stack-trace and
print-exception functions that handle
Java exceptions. For example
(define-generic-java-method char-at)
(with/fc (lambda (m e) (print-exception (make-exception m e)))
(lambda () (char-at (->jstring "foo") (->jint 3))))
will catch the
IndexOutOfBoundsException, print its
stack trace and return #f.
In Scheme, Java exceptions can be thrown by raising an error containing the Java exception as the message, e.g.
(define-java-class <java.util.no-such-element-exception>)
(error (java-new <java.util.no-such-element-exception>))
or
(throw (make-error (java-new <java.util.no-such-element-exception>)))
If this occurs inside a proxy method (see the section called “Proxies”), the exception is propagated to the invoking Java code.
Invoking [define-]java-class[es],
java-new or any of the procedures defined
with
[define-]generic-java-{method,field-accessor,field-modifier}[s]
causes S2J to perform reflection on the named Java class(es),
the class passed as the first argument, or the class
corresponding to the type first argument passed to the other
procedures, respectively. This process collects information
about all the constructors, methods and fields of the class
and its superclasses/interfaces.
The only class members processed during this automatic reflection are public ones declared in public classes. This almost exactly mimics the visibility rules in Java for code residing in packages other than the one the member is residing in. It is also in line with the default permissions granted to the Java reflection API. There is one rare case where this rule is more restrictive than Java's: public members of package-protected classes are not visible even when accessed via a public sub-class.
Depending on the security settings, the Java reflection API is in fact capable of granting access to any members of any class. However, using this in the automatic reflection performed by S2J would constitute a significant departure from normal Java behaviour and result in unpredictable results to the user. For instance, undocumented private methods would be invoked in preference to documented public methods if the formers type signature provided a better match.
Automatic reflection ignores security exceptions thrown by the Java reflection API, i.e. the class in question will appear to have no constructors, methods and fields. This is designed to cope with situations where the default security settings have been altered in a way that prevents access to members of some (or even all) classes.
In some applications the reflection API permissions depend on
the context of the invocation. For instance, in applets it is
usually possible to access class member information as part of
the initialisation but not after that. Since
[define-]java-class[es] triggers
automatic reflection, it can be used to control when automatic
reflection for specific classes takes place.
This section provides a summary of all the commonly used S2J
features, correlating them with the corresponding Java
code. It makes use of some functions from the
srfi-1, srfi-26 and
misc modules
(require-library 'sisc/libs/srfi)
(import* srfi-1 fold)
(import* srfi-26 cut cute)
(import* misc compose)
Table 8.3. Common S2J Usage
| Java | Scheme |
|---|---|
| create bindings for classes, methods and fields | |
n/a
|
(define-java-classes <foo.bar-baz> <foo.bar-boo>)
(define-generic-java-methods get-bar get-baz set-bar! set-baz!)
(define-generic-java-field-accessors :bar :baz)
(define-generic-java-field-modifiers :bar! :baz!)
|
| instantiate class | |
foo.BarBaz fooObj = new foo.BarBaz(a, b, c);
|
(define foo-obj (java-new <foo.bar-baz> a b c))
|
| invoke method on instance | |
Object res = fooObj.barBaz(a, b, c)
|
(define res (bar-baz foo-obj a b c))
|
| invoke method on class | |
Object res = foo.Bar.baz(a, b, c)
|
(define res (baz (java-null <foo.bar>) a b c))
|
| access instance field | |
Object res = fooObj.bar;
|
(define res (:bar foo-obj))
|
| access class field | |
Object res = foo.Bar.baz;
|
(define res (:bar (java-null <foo.bar>)))
|
| modify instance field | |
fooObj.bar = val;
|
(:bar! foo-obj val)
|
| modify class field | |
foo.Bar.baz = val;
|
(:bar! (java-null <foo.bar>) val)
|
| chained field access | |
Object res = fooObj.bar.baz.boo
|
(define res (fold (cut <> <>) foo-obj (list :bar :baz :boo)))
or
(define res ((compose :boo :baz :bar) foo-obj))
This works equally well for bean fields.
|
| chained field modification | |
fooObj.bar.baz.boo = moo;
|
(:boo! (fold (cut <> <>) foo-obj (list :bar :baz)) moo)
or
(:boo! ((compose :baz :bar) foo-obj) moo)
This works equally well for bean fields.
|
| accessing several fields | |
a = fooObj.bar;
b = fooObj.baz;
c = fooObj.boo;
|
(apply (lambda (a b c) ...)
(map (cute <> foo-obj) (list :bar :baz :boo)))
This works equally well for bean fields.
|
| modifying several fields | |
fooObj.bar = a;
fooObj.baz = b;
fooObj.boo = c;
|
(for-each (cute <> foo-obj <>)
(list :bar! :baz! :boo!)
(list a b c))
This works equally well for bean fields.
|
| creating an array | |
int[][] ar = new int[2][2];
|
(define ar (java-array-new <jint> '(2 2)))
This works equally well for bean fields.
|
| accessing an array element | |
int res = ar[1][1];
|
(define res (java-array-ref ar '(1 1)))
|
| modifying an array element | |
ar[1][1] = val;
|
(java-array-set! ar '(1 1) val)
|
| iterating over an array | |
for(int i=0; i<ar.length; i++) ar[i].fooBar(a,b);
|
(for-each (cute foo-bar <> a b) (->list ar))
|
| implementing interfaces | |
public class Foo implements Bar, Baz {
private int x;
private int y;
public Foo(int x, int y) {
this.x = x;
this.y = y;
}
public int barMethod(int z) {
return x+y+z;
}
public int bazMethod(int z) {
return x+y-z;
}
}
...
Foo fooObj = new Foo(1, 2);
|
(define-java-proxy (foo x y)
(<bar> <baz>)
(define (bar-method p z)
(->jint (+ x y (->number z))))
(define (baz-method p z)
(->jint (+ x y (- (->number z))))))
...
(define foo-obj (foo 1 2))
|
Requires: (import s2j)
The S2J Reflection API lets Scheme code access all the core functions of the Java reflection API. It underpins the High Level S2J Interface (see the section called “Calling Java from Scheme”). Normal interaction with Java from Scheme does not require knowledge of this API, just like normal use of Java does not require knowledge of the Java reflection API.
These functions access attributes and members of classes.
procedure:
(java-class-name jclass) => symbolReturns the name of
jclass.
procedure:
(java-class-flags jclass) => list of symbolsReturns the modifiers of
jclass, for examplepublic static final.
procedure:
(java-class-declaring-class jclass) => jclassReturns the Java class in which
jclasswas declared, or null if it was declared at the top level.
procedure:
(java-class-declared-superclasses jclass) => list of jclassReturns the direct superclasses of
jclass.Normally this is the class' superclass followed by all of its interfaces in the order they were specified in the class declaration. There are a number of exceptions which ensure that the result is consistent with the precedence order employed by Java for method lookup on overloaded method. Interfaces and classes that directly inherit from
java.lang.Objectare all givenjava.lang.Objectas the last element in their superclass list. For primitive and array types the direct superclass or superclasses reflect the widening conversions performed by Java. For example,<jint>'s superclass is<jlong>and<java.util.array-list[][]>'s superclasses are:
<java.util.abstract-list[][]>
<java.util.list[][]>
<java.util.random-access[][]>
<java.lang.cloneable[][]>
<java.io.serializable[][]>Note that this behavior is different from the corresponding method in the Java reflection API.
procedure:
(java-class-declared-classes jclass) => list of jclasses/#fReturns all the classes declared by
jclass, or#fif access to this information is prohibited.
procedure:
(java-class-declared-constructors jclass) => list of jconstructor/#fReturns all the constructors declared by
jclass, or#fif access to this information is prohibited.
procedure:
(java-class-declared-methods jclass) => list of jmethods/#fReturns all the methods declared by
jclass, or#fif access to this information is prohibited.
procedure:
(java-class-declared-fields jclass) => list of jfields/#fReturns all the fields declared by
jclass, or#fif access to this information is prohibited.
procedure:
(java-class-precedence-list jclass) => list of jclassesReturns the total order of
jclassand all direct and indirect superclasses, as determined by the partial orders obtained from callingjava-class-declared-superclasses.The class precedence list is important when comparing types using the type system's
compare-typesprocedure, which is used by the generic procedure method selection algorithm (seecompare-methodsin the section called “Invoking Generic Procedures”). Since generic Java methods and field accessors/mutators are implemented in terms of generic procedures they are all affected by the class precedence list.
procedure:
(java-constructor? value) => #t/#fDetermines whether
valueis a Java constructor.
procedure:
(java-constructor-name jconstructor) => symbolReturns the name of
jconstructor.
procedure:
(java-constructor-flags jconstructor) => list of symbolsReturns the modifiers of
jconstructor, such aspublic static final.
procedure:
(java-constructor-declaring-class jconstructor) => jclassReturns the Java class in which
jconstructorwas declared.
procedure:
(java-constructor-parameter-types jconstructor) => list of jclassesReturns the declared types of the parameters of
jconstructor.
procedure:
(java-constructor-procedure jconstructor) => procedureReturns a procedure that when called invokes the constructor with the passed arguments, returning the newly created objected.
procedure:
(java-constructor-method jconstructor) => methodReturns a method suitable for adding to generic procedures that, when called invokes the underlying Java constructor with the passed arguments. The resulting newly created object is returned.
procedure:
(java-method? value) => #t/#fDetermines whether
valueis a Java method.
procedure:
(java-method-name jmethod) => symbolReturns the name of
jmethod.
procedure:
(java-method-flags jmethod) => list of symbolsReturns the modifiers of
jmethod, such aspublic static final.
procedure:
(java-method-declaring-class jmethod) => jclassReturns the Java class in which
jmethodwas declared.
procedure:
(java-method-parameter-types jmethod) => list of jclassesReturns the declared types of the parameters of
jmethod.
procedure:
(java-method-procedure jmethod) => procedureReturns a procedure that when called invokes the method with the passed arguments, returning the newly created objected.
procedure:
(java-method-method jmethod) => methodReturns a method suitable for adding to generic procedures that, when called invokes the underlying Java method on the object passed as the first argument, and with the remaining arguments passed as parameters. The result of the method invocation is returned. Static methods can be invoked by passing a typed null object as the first parameter to the generic procedure.
procedure:
(java-field? value) => #t/#fDetermines whether
valueis a Java field.
procedure:
(java-field-name jfield) => symbolReturns the name of
jfield.
procedure:
(java-field-flags jfield) => list of symbolsReturns the modifiers of
jfield, such aspublic static final.
procedure:
(java-field-declaring-class jfield) => jclassReturns the Java class in which
jfieldwas declared.
procedure:
(java-field-type jfield) => jclassReturns the declared type of
jfield.
procedure:
(java-field-accessor-procedure jfield) => procedureprocedure:
(java-field-modifier-procedure jfield) => procedureReturns a procedure that when called returns or sets (respectively) the value of the field on the object specified by the first parameter to the invocation. Static fields can be accessed/modified by passing a null object.
procedure:
(java-field-accessor-method jfield) => methodprocedure:
(java-field-modifier-method jfield) => methodReturns a method suitable for adding to generic procedures that, when called returns/sets the value of the field on the object specified by the first argument to the generic procedure invocation. Static fields can be accessed/modified by passing a typed null object as the first parameter to the generic procedure.
procedure:
(java-array-class jclass dimensions) => jclassReturns a class representing the array type that has
jclassas the component type anddimensionsas the number of dimensions. For example, the following expressions are equivalent:(java-array-class <jint> 2) (java-class '|int[][]|)
The list of direct superclasses returned by
java-class-declared-superclassesfor an array class is consistent with the widening conversion performed by Java, e.g. the direct superclasses ofjava.util.ArrayList[][]are:
java.util.AbstractList[][]
java.util.List[][]
java.util.RandomAccess[][]
java.lang.Cloneable[][]
java.io.Serializable[][]This is different from what the Java reflection APIs return.
procedure:
(java-proxy-class jinterface) => jclassCreates a Java class that implements the specified interfaces. The class can be instantiated with an invocation handler, such as the one returned by
java-proxy-dispatcherbelow, that delegates method invocation to Scheme code.
procedure:
(java-proxy-dispatcher alist) => invocation-handlerCreates an invocation handler suitable for use in the instantiation of a proxy (see
java-proxy-classabove). The keys inalistare Java method names and the values are Scheme procedures.When a method is invoked on a proxy, the procedure matching the method's name is invoked with the proxy object and the parameters of the method invocation as arguments. The result of the invocation is returned as the result of the method call. If
alistdoes not contain a binding for the method name, an error is signalled.