All Manuals > LispWorks® User Guide and Reference Manual > 39 The LW-JI Package

define-lisp-proxy Macro

Summary

Defines a Lisp proxy.

Package

lw-ji

Signature

define-lisp-proxy name &body interface-and-method-descs => name

Arguments
name
A non-nil symbol.
interface-and-method-descs
A body of Lisp code.
Values
name
A non-nil symbol.
Description

The macro define-lisp-proxy defines a Lisp proxy, which means creating a Lisp proxy definition and attaching it to name, which can then be used to create Lisp proxies, which are Java proxies where methods invocation ends up calling Lisp functions.

define-lisp-proxy parses interface-and-method-descs to a proxy definition, and attaches it to name. This operation is a "load-time" operation: it does not require running Java, and does not create any proxy. The name can then be used at run time as argument to make-lisp-proxy or make-lisp-proxy-with-overrides, or to the Java method com.lispworks.LispCalls.createLispProxy. The result of any these calls is a proxy that implements the interfaces listed in interface-and-method-descs, and can be used in Java whenever an object that implements any of these interfaces is required.

interface-and-method-descs describes the Java interfaces to implement and the Lisp functions to call. It is parsed as a body of Lisp forms.

Each element in the list must be either a string which is the Java interface name, or a list where the cl:car is the Java interface name. Each item specifies a Java interface to implement, except that one item (at most) may specify options relating to the whole proxy definition, by using a list starting with the keyword :options instead of giving an interface name.

When the item is a list starting with an interface name, the rest of the list are method specifications. Note that you do not need to have a method specification for each method of the interface.

Each method specification must be a list, where the first element is a string with the name of the Java method, and the second element is the symbol specifying what Lisp function to call for this method. The symbol specifies the function to call except when it is overridden (see below about "Overriding"). In some cases, you will want to always override the function to call (typically when you want to use a closure as the function), in which case the symbol can be and should be a keyword (which is ignored by the verifying functions), but does not have to be. See below for how the calling of the Lisp function is done.

The rest of the method specification can contain keyword/value pairs. Currently, the only supported keyword is :with-user-data, which takes a boolean value, overrides the default value of :with-user-data of the proxy definition. The default of :with-user-data of the definition defaults to nil, and can be changed in the :options. The value of :with-user-data specifies whether to pass the user-data of a proxy to the Lisp function.

The :options item is specified by an item in interface-and-method-descs where the cl:car is the keyword :options. The rest of the item is keyword/value pairs. The keywords currently supported are:

:default-function

Specifies the default function to call for methods which do not have a Lisp function. This function is applied to the arguments of the method preceded by the method-name, and if :default-function-with-user-data is non-nil also with user-data preceding the method-name.

The default function can be overridden by make-lisp-proxy and make-lisp-proxy-with-overrides.

:default-function-with-user-data

A boolean specifying whether the user-data of a proxy should be passed when the default function is called. When it is non-nil, the user-data is passed as the first argument to the default-function (or the function that overrides it). The default value of :default-function-with-user-data is nil.

:with-user-data

A boolean specifying whether the default for calling functions in the proxy definition is with user-data or not. Each method description can override it as described above. The default value of :with-user-data is nil.

:print-name

Must be a string or a symbol. Specifies the first part of the print-name of each proxy.

:jobject-scope

One of :global, :local or nil. This controls the scope of jobject arguments (that is, arguments that are not of primitive type or string). With the default value :global, jobjects are passed as global jobjects and can be used indefinitely. When :jobject-scope is :local, jobjects are passed as a local jobject, which means that they must not be used outside the scope of the function that is invoked by the proxy. Using a local jobject out of scope can cause the system to crash (rather than call cl:error). When :jobject-scope is nil, jobjects are not passed at all to the functions. Note that means that the number of arguments that the functions in the proxy receive is different when :jobject-scope is nil, because only arguments of primitive type or strings are passed.

If you use :jobject-scope :local, the function can convert it to global using jobject-ensure-global, and then it can be used out of scope.

The default value of :jobject-scope is :global.

user-data

The user-data is set up for each individual proxy object by make-lisp-proxy or make-lisp-proxy-with-overrides, and thus allows you to associate each individual proxy with an arbitrary Lisp object. The proxy definition determines whether to use it when calling the Lisp functions in the proxy definition. The default value of user-data is nil, so if you want to use it you need to specify it by using :with-user-data, either in the :options which would give the default value for all calls in the definition, or in individual method specifications. When user-data is passed, it is always passed to the Lisp function as the first argument. Another way to individualize proxies is to use overriding, which also allows you to use closures.

Overriding

When make-lisp-proxy or make-lisp-proxy-with-overrides make a proxy, they can specify overriding of some of the symbols in the proxy definition. Overriding here means mapping one symbol to to another symbol or a function object. When a symbol is supposed to be called and it is overridden, the target of the mapping is called rather than the symbol. Note that the overriding is specific to each individual proxy rather to the proxy definition, and therefore you can have different proxies using the same proxy definition (and hence implementing the same interfaces), but calling different Lisp functions. An advantage of overriding is that it allows you to use closures created at run time instead of symbols.

See the documentation for make-lisp-proxy for how the overriding is created.

Calling the Lisp function

After a proxy is created from a proxy definition, any invocation of a Java method on it (except the Object methods toString, equals and hashCode) enters Lisp.

When a method is invoked on a proxy (normally from Java, but can be done from Lisp too), the steps for invoking your Lisp function are:

  1. Check whether the item in interface-and-method-descs for the interface of the method contains a method specification with the method-name of the method.
  2. Convert the Java method arguments to Lisp arguments where possible. See 15.1 Types and conversion between Lisp and Java.

    Note that that if the first step above found a method specification, and it contains the keyword :jobject-scope, it affects the way non-primitive arguments are processed as described above.

  3. Calling the user code:

    (i) If a method specification was found in the first step above:

    (a) Take the symbol from the method specification, then:

    (b) Check whether the symbol is overridden, and if it is use the target as a function to call. Otherwise, check whether the symbol is fbound, and if it is use it as a function to call, then:

    (c) If as a result of (b) there is a function to call, check whether it should be called with the user-data. If :with-user-data was used in the method specification then use its value, otherwise if :with-user-data was used in :options item use this value, otherwise default to nil, then:

    (d) Apply the function: if using user-data, apply the function to the user-data followed by the Lisp arguments, otherwise apply the function to the Lisp arguments only.

    (ii) If the method-specific call in (i) did not happen (no method specification found, or the symbol is not fbound and not overridden), try to apply the default function:

    (a) If there is a default function, check whether it is overridden and if so use the target as the function to call. Otherwise use the default-function itself as the function to call, then:

    (b) Check whether need to pass user-data, which is specified by the :default-function-with-user-data in the :options item, then:

    (c) Apply the function: if user-data needs to be used, apply the function to the user-data, method-name and the Lisp arguments. Otherwise apply the function to the method-name and Lisp arguments.

    (iii) If the calls in (i) and (ii) did not happen, an error is signaled. See handling of errors below.

  4. Return a value: currently, if the user function returned a Java object (a jobject or an instance of standard-java-object), it is returned without checking. Otherwise, try to convert it to the appropriate Java object and return it. Otherwise, report it by calling the java-to-lisp-debugger-hook (see init-java-interface) with a cl:simple-error condition and return a default value from the Java method invocation, which is 0 for primitive types or null for other types.
Throwing out and error handling

The call to the Lisp function is wrapped dynamically such that any throw from it is blocked, and the default value as in the last step above is returned.

In addition, there is a debugger wrapper (using with-debugger-wrapper) which calls the java-to-lisp-debugger-hook (see init-java-interface) with the condition and then calls cl:abort. If this abort is not caught by your cl:abort restart, it is handled by the "throwing blocker" from the previous paragraph, that is the Java method returns 0 or null.

Verification

The verification functions verify-lisp-proxies and verify-lisp-proxy are provided to allow you to do some checking of the correctness of your proxy definition. Two things can be verified:

Note that neither of these issues is actually an error, because the default function can handle them. However it is useful to check them in case you did miss something.

Performance issues

There is a little overhead associated with using setup-lisp-proxy as opposed to define-lisp-proxy, both in the size of the delivered application (very small) and in run time, but the difference is not large enough to prevent using setup-lisp-proxy when it is appropriate.

There is an overhead associated with initializing a proxy definition. It is therefore a bad idea to use setup-lisp-proxy many times.

Overrides and using multiple interfaces add a negligible overhead.

:jobject-scope with nil or :local are useful optimizations. In proxies that are invoked infrequently, say less than 10 times each second, the difference is probably insignificant, but it is useful for proxies that are called repeatedly by Java code. For example, if you implement the interface "java.io.FilenameFilter" to pass to "java.io.File.list" on large directories, using :jobject-scope :local or nil will reduce the overhead significantly.

Examples
(example-edit-file "android/android-othello-user")
See also

make-lisp-proxy
make-lisp-proxy-with-overrides
verify-lisp-proxy
verify-lisp-proxies
check-lisp-calls-initialized
15.3.2 Using proxies
15.3.1 Direct calls


LispWorks® User Guide and Reference Manual - 01 Dec 2021 19:30:46