Java Interoperability
Calling Java Code from PicoLisp
Why not interface PicoLisp directly to Java? We have that already in Ersatz PicoLisp, the plain vanilla Java version of PicoLisp. It is almost trivial to port this functionality to real PicoLisp, by starting up and communicating with a Java virtual machine (JVM).For universal use I added a "java/" directory to the PicoLisp core distribution (with version 3.1.9.7). Please note that because of differences in the PLIO format of external symbols, it is fully supported only on the 64-bit version of PicoLisp.
Startup
Naturally, a Java runtime (JRE), and optionally - if you want to use inline Java code (see below) - a development kit (JDK) should be installed.An application needs to call
(load "@java/lib.l")For the following experiments, you can start PicoLisp as
$ pil @java/lib.l +
The API
Just as in ErsatzLisp, java is the central function. The syntax is very similar, with one major difference: The Java library distinguishes more strictly between internal and transient symbols. Methods are recognized when they are passed as internal symbols, while class names should be transients.The primitive types are passed directly. This means that transient symbols on the PicoLisp side map to String on the Java side, and PicoLisp numbers map - depending on the size - to int or BigInteger. Other number types see "Extended Data Formats" below. The Lisp symbols T and NIL map to Boolean.
Java Objects appear on the PicoLisp side as external symbols, like remote database objects, by making use of Java reflection and the PicoLisp *Ext mechanism. This allows transparent (read-only) access to Java objects with arbitrary Lisp functions like get, show, edit etc.
To create a new Java object (constructor call):
(java 'cls 'T ['any ..]) -> obj
Call a static method msg for a class cls:
(java 'cls 'msg ['any ..]) -> obj
Call a dynamic method msg for an object obj
(java 'obj 'msg ['any ..]) -> obj
Like in ErsatzLisp, public accesses public fields in Java objects or classes:
(public 'obj 'any ['any ..]) -> objreturns the value of a public field any in object obj.
(public 'cls 'any ['any ..]) -> objreturns the value of a public field any in class cls.
In both forms, the optional any arguments will in turn access corresponding fields in the object retrieved so far.
To create an interface object:
(interface 'obj 'cls|lst 'sym 'fun ..) -> objcreates an interface, i.e. a set of methods for a class cls (or a list of classes lst), and associates it with the object obj. Note that the obj argument does not in ErsatzLisp.
Examples
From the above article:: (setq Sb (java "java.lang.StringBuilder" T "abc")) # Construct object -> {OOOO5276732765} : (java Sb 'append (char 44)) # Call 'append' method -> {OOOO5276732765} : (java Sb 'append 123) -> {OOOO5276732765} : (java Sb 'toString) # Convert to String -> "abc,123"This example is a bit silly, as strings are better handled directly in PicoLisp.
But a general calendar object might be useful:
: (setq Cal (java "java.util.GregorianCalendar" T)) -> {OOOO24137612300}We can inspect it with show:
: (show Cal) {OOOO24137612300} ({OOOO7764433062} . "java.util.GregorianCalendar") BC 0 BCE 0 AD 1 CE 1 EPOCH_OFFSET 719163 EPOCH_YEAR 1970 MONTH_LENGTH (31 28 31 30 31 30 31 31 30 31 30 31) LEAP_MONTH_LENGTH (31 29 31 30 31 30 31 31 30 31 30 31) ONE_SECOND 1000 ONE_MINUTE 60000 ONE_HOUR 3600000 ONE_DAY 86400000 ONE_WEEK 604800000 MIN_VALUES (0 1 0 1 0 1 1 1 1 0 0 0 0 0 0 -46800000 0) LEAST_MAX_VALUES (1 292269054 11 52 4 28 365 7 4 1 11 23 59 59 999 50400000 1200000) MAX_VALUES (1 292278994 11 53 6 31 366 7 6 1 11 23 59 59 999 50400000 7200000) serialVersionUID -8125100834729963327 gcal {OOOO2322434213} jcal NIL jeras NIL DEFAULT_GREGORIAN_CUTOVER -12219292800000 gregorianCutover -12219292800000 gregorianCutoverDate 577736 gregorianCutoverYear 1582 gregorianCutoverYearJulian 1582 gdate {OOOO34261410206} cdate {OOOO34261410206} calsys {OOOO2322434213} zoneOffsets (3600000 0) originalFields NIL cachedFixedDate 735631 $assertionsDisabled T -> {OOOO24137612300}look at its class and superclass,
: (show '{OOOO7764433062}) {OOOO7764433062} ({OOOO376765756} . "java.util.Calendar") ... : (show '{OOOO376765756}) {OOOO376765756} ({OOOO1514706475} . "java.lang.Object") -> {OOOO376765756}or operate on it
: (java (java Cal 'getTimeZone) 'getDisplayName) # Get the time zone -> "Central European Time" : (java Cal 'get (public Cal 'HOUR_OF_DAY)) # Current hour -> 10 : (time (time)) # Check it -> (10 34 0) : (java Cal 'get (public Cal 'WEEK_OF_YEAR)) # Current week -> 6 : (week (date)) # Check it -> 6and change values:
: (java Cal 'get (public Cal 'YEAR)) # Current year -> 2015 : (java Cal 'set (public Cal 'YEAR) 2020) # Modify it -> NIL : (java Cal 'get (public Cal 'YEAR)) -> 2020
Simple GUI Examples
The source codes of the following three examples can also be downloaded from http://software-lab.de/javaDemo.tgz.Again from the article:
(let (Frame (java "javax.swing.JFrame" T "Bye-Frame") Button (java "javax.swing.JButton" T "OK") ) (java Frame 'add "South" Button) (java Button 'addActionListener (interface Button "java.awt.event.ActionListener" # When button is clicked, 'actionPerformed '((Ev) (bye)) ) ) # Exit PicoLisp (java Frame 'setSize 100 60) (java Frame 'setVisible T) )It shows a use case for interface.
The "Animation" task from http://rosettacode.org:
(de animate (Str) (let (Txt (chop Str) Dir 1 Rev (dec (length Txt)) Frame (java "javax.swing.JFrame" T "Animation") Label (java "javax.swing.JLabel" T Str) ) (java Label 'addMouseListener (interface Label "java.awt.event.MouseListener" 'mouseClicked '((Ev) (setq Dir (if (= 1 Dir) Rev 1))) ) ) (java Frame 'add Label) (java Frame 'pack) (java Frame 'setVisible T) (until (key 250) (java Label 'setText (pack (do Dir (rot Txt)))) ) (java Frame 'dispose) ) )You can call it as
(animate "Hello World! ")It will show a scrolling text, which reverses the scroll direction when you click on it. A key press stops it.
Inline Java Code
Like in ErsatzLisp, you can embed Java Programs directly into PicoLisp code, with the javac function:### RosettaCode Image Noise ### (javac "ImageNoise" "JPanel" NIL "java.util.*" "java.awt.*" "java.awt.image.*" "javax.swing.*" ) int DX, DY; int[] Pixels; MemoryImageSource Source; Image Img; Random Rnd; public ImageNoise(int dx, int dy) { DX = dx; DY = dy; Pixels = new int[DX * DY]; Source = new MemoryImageSource(DX, DY, Pixels, 0, DX); Source.setAnimated(true); Img = createImage(Source); Rnd = new Random(); } public void paint(Graphics g) {update(g);} public void update(Graphics g) {g.drawImage(Img, 0, 0, this);} public void draw() { for (int i = 0; i < Pixels.length; ++i) { int c = Rnd.nextInt(255); Pixels[i] = 0xFF000000 | c<<16 | c<<8 | c; } Source.newPixels(); paint(getGraphics()); } /**/ (de imageNoise (DX DY Fps) (let (Dly (/ -1000 Fps) Frame (java "javax.swing.JFrame" T "Image Noise") Noise (java "ImageNoise" T DX DY) Button (java "javax.swing.JButton" T "OK") ) (java Frame 'add Noise) (java Frame 'add "South" Button) (java Button 'addActionListener (interface Button 'java.awt.event.ActionListener 'actionPerformed (curry (Dly Frame) (Ev) (task Dly) (java Frame 'setVisible NIL) #{? (java Frame 'dispose) }# ) ) ) (java Frame 'setSize DX DY) (java Frame 'setVisible T) (task Dly 0 Obj Noise (java Obj 'draw) ) ) )javac is analog to the gcc function in "@lib/native.l".
It takes a package or class name (here "ImageNoise"), an optional "extends" (here "JPanel") and "implements" (here empty), and an arbitrary number of "import" arguments (here "java.util.*" etc.).
Then all following code till a separating "/**/" line is taken as Java code, creating a file "ImageNoise.class" in the temporary (process-private) space. You can start it as (imageNoise 320 240 25). A window is shown with random image noise, and an "OK" button to close it.
Extended Data Formats
While integers are directly passed to/from the JVM, other number formats can be passed to the JVM as cons pairs consisting of an ID in the CAR and the value in the CDR. These are:- 1 for Byte
- 2 for Char
- 3 for Short
- 4 for Integer (default, usually not needed)
- 8 for Long
- 9 for BigInteger
: (scl 6) -> 6 : (round (public "java.lang.Math" 'PI)) -> "3.142" : (round (java "java.lang.Math" 'pow (-6 . 4.0) (-6 . 3.0))) -> "64.000"
If the argument is a list, it is passed as an array of the corresponding type to/from the JVM.
See the file "@java/test.l" in the PicoLisp distribution for more examples.
How It Works
The top-level functions java, public, interface and reflect in "@java/lib.l" call the internal workhorse jre.The runtime Java code is in "@java/reflector.jar". It was built with
$ (cd java; ./mkJar)from the sources in "Reflector.java" (the actual communication with PicoLisp) and "InOut.java" (a PLIO library).
jre checks a global variable *Java. If this variable is not set, it is assumed that the JVM is not running yet. It will be started by the line
(pipe (exec 'java "-cp" Path "Reflector" Rqst Rply))This runs Java with the proper classpath in Path, and the bidirectional file descriptor (for the pipes to standard input and from standard output of that process) is stored in *Java, together with a fixed external symbol offset of 65535 (more below). This pipe is then used to send commands to the JVM, and receive results.
Before that, two additional pipes Rqst (request) and Rply (reply) are created as named pipes (with mkfifo), and passed to the Java-call on the command line. They are used for asynchronous requests from Java back to PicoLisp, allowing the execution of Lisp code from Java (e.g. for the interface callbacks).
A background task is started to handle these requests.
Then a cons pair of that external symbol offset (65535, in the CAR of Java) and the reflect function is added to the PicoLisp system global *Ext. This allows to receive and send Java objects like remote database objects. The highest possible file number of 65535 was chosen to avoid conflicts with possibly open local and remote databases.
Whenever data is received from Java (with rd), or sent with pr, the corresponding ext-calls make handle the proper symbol offsets.
After *Java is initialized, subsequent calls to jre will only execute
(ext (car *Java) (out (cdr *Java) (pr (rest))) (in (cdr *Java) (rd)) ) )i.e. send all arguments to Java with out), and receive the results with in).
https://picolisp.com/wiki/?javacode
20mar16 | erik |