Add ABCL disassembler post.
[blog.git] / abcl-and-lire.post
1 ;;;;;
2 title: Setting up ABCL and LIRE
3 date: 2015-11-11 20:34:29
4 format: md
5 tags: lisp
6 bindings: 3bmd-code-blocks:*code-blocks-default-colorize* :common-lisp
7 ;;;;;
8
9 ## Intro
10
11 The purpose of this article is to examine how using ABCL with existing
12 libraries (arguably *the* main point of using ABCL at the moment)
13 actually looks like in practice.  Never mind integration with Spring, or
14 other more involved frameworks, this will only touch a single library
15 and won't require us to write from-Java-callable classes.
16
17 In the process of refining this I'm hoping to also get ideas about the
18 requirements for building a better DSL for the Java FFI, based on the
19 intended "look" of the code (that is, coding by wishful thinking).
20
21 ## Setup
22
23 Ensuring the correct package is somewhat optional:
24
25 ```
26 (in-package #:cl-user)
27 ```
28
29 Generally using JSS is a bit nicer than the plain Java FFI.  After the
30 contribs are loaded, JSS can be required and used:
31
32 ```
33 (require '#:abcl-contrib)
34 (require '#:jss)
35
36 (use-package '#:jss)
37 ```
38
39 Next, we need access to the right libraries.  After building LIRE from
40 source and executing the `mvn dist` command we end up with a JAR file
41 for LIRE and several dependencies in the `lib` folder.  All of them need
42 to be on the classpath:
43
44 ```
45 (progn
46   (add-to-classpath "~/src/LIRE/dist/lire.jar")
47   (mapc #'add-to-classpath (directory "~/src/LIRE/dist/lib/*.jar")))
48 ```
49
50 ## Prelude
51
52 Since we're going to read pictures in a couple of places, a helper to
53 load one from a pathname is a good start:
54
55 ```
56 (defun read-image (pathname)
57   (#"read" 'javax.imageio.ImageIO (new 'java.io.File (namestring pathname))))
58 ```
59
60 To note here is the use of `NEW` from JSS with a symbol for the class
61 name, the conversion of the pathname to a regular string, since the Java
62 side doesn't expect a Lisp object and the `#""` reader syntax from JSS
63 to invoke the method `read` in a bit of a simpler way than using the FFI
64 calls directly.
65
66 JSS will automatically "import" Java names, so the same function can
67 simply be the following instead (provided that the names aren't
68 ambiguous):
69
70 ```
71 (defun read-image (pathname)
72   (#"read" 'ImageIO (new 'File (namestring pathname))))
73 ```
74
75 The names will be looked up again on every call though, so this option
76 isn't the best performing one.
77
78 For comparison, the raw FFI would be a bit more verbose, but explicitely
79 specifies all names:
80
81 ```
82 (defun read-image (pathname)
83   (jstatic "read" "javax.imageio.ImageIO" (jnew "java.io.File" (namestring pathname))))
84 ```
85
86 Though with a combination of JSS and cached lookup it could be nicer,
87 even though the setup is more verbose:
88
89 ```
90 (defvar +image-io+ (jclass "javax.imageio.ImageIO"))
91 (defvar +file+ (jclass "java.io.File"))
92
93 (defun read-image (pathname)
94   (#"read" +image-io+ (jnew +file+ (namestring pathname))))
95 ```
96
97 At this point without other improvements (auto-coercion of pathnames,
98 importing namespaces) it's about as factored as it will be (except
99 moving every single call into its own Lisp wrapper function).
100
101 ## Building an index
102
103 To keep it simple building the index will be done from a list of
104 pathnames in a single step while providing the path of the index as a
105 separate parameter:
106
107 ```
108 (defun build-index (index-name pathnames)
109   (let ((global-document-builder
110           (new 'GlobalDocumentBuilder (find-java-class 'CEDD)))
111         (index-writer (#"createIndexWriter"
112                        'LuceneUtils
113                        index-name
114                        +true+
115                        (get-java-field 'LuceneUtils$AnalyzerType "WhitespaceAnalyzer"))))
116     (unwind-protect
117          (dolist (pathname pathnames)
118            (let ((pathname (namestring pathname)))
119              (format T "Indexing ~A ..." pathname)
120              (let* ((image (read-image pathname))
121                     (document (#"createDocument" global-document-builder image pathname)))
122                (#"addDocument" index-writer document))
123              (format T " done.~%")))
124       (#"closeWriter" 'LuceneUtils index-writer))))
125 ```
126
127 **Note**: This code won't work on current ABCL as is, because the lookup
128 is disabled for for nested classes (those containing the dollar
129 character).  Because of this, the `AnalyzerType` class would have to be
130 looked up as follows:
131
132 ```
133 (jfield "net.semanticmetadata.lire.utils.LuceneUtils$AnalyzerType" "WhitespaceAnalyzer")
134 ```
135
136 All in all nothing fancy, JSS takes care of a lot of typing as the names
137 are all unique enough.
138
139 The process is simply creating the document builder and index writer,
140 reading all the files one by one and adding them to the index.  There's
141 no error checking at the moment though.
142
143 To note here is that looking up the precise kind of a Java name is a bit
144 of a hassle.  Of course intuition goes a long way, but again, manually
145 figuring out whether a name is a nested class or static/enum field is
146 annoying enough since it involves either repeated calls to `JAPROPOS`,
147 or reading more Java documentation.
148
149 Apart from that, this is mostly a direct transcription.  Unfortunately
150 written this way there's no point in creating a `WITH-OPEN-*` macro to
151 automatically close the writer, however, looking at the `LuceneUtils`
152 source this could be accomplished by directly calling `close` on the
153 writer object instead - a corresponding macro might this then:
154
155 ```
156 (defmacro with-open ((name value) &body body)
157   `(let ((,name ,value))
158      (unwind-protect
159           (progn ,@body)
160        (#"close" ,name))))
161 ```
162
163 It would also be nice to have auto conversion using keywords for enum
164 values instead of needing to look up the value manually.
165
166 ## Querying an index
167
168 The other way round, looking up related pictures by passing in an
169 example, is done using an image searcher:
170
171 ```
172 (defun query-index (index-name pathname)
173   (let* ((image (read-image pathname))
174          (index-reader (#"open" 'DirectoryReader
175                                 (#"open" 'FSDirectory
176                                          (#"get" 'Paths index-name (jnew-array "java.lang.String" 0))))))
177     (unwind-protect
178          (let* ((image-searcher (new 'GenericFastImageSearcher 30 (find-java-class 'CEDD)))
179                 (hits (#"search" image-searcher image index-reader)))
180            (dotimes (i (#"length" hits))
181              (let ((found-pathname (#"getValues" (#"document" index-reader (#"documentID" hits i))
182                                                  (get-java-field 'builders.DocumentBuilder "FIELD_NAME_IDENTIFIER"))))
183                (format T "~F: ~A~%" (#"score" hits i) found-pathname))))
184       (#"closeReader" 'LuceneUtils index-reader))))
185 ```
186
187 To note here is that the `get` call on `java.nio.file.Paths` took way
188 more time to figure out than should've been necessary:  Essentially the
189 method is using a variable number of arguments, but the FFI doesn't help
190 in any way, so the array (of the correct type!) needs to be set up
191 manually, especially if the number of variable arguments is zero.  This
192 is not obvious at first and also takes unnecessary writing.
193
194 The rest of the code is straightforward again.  At least a common
195 wrapper for the `length` call would be nice, but since the result object
196 doesn't actually implement a collection interface, the point about
197 having better collection iteration is kind of moot here.
198
199 ## A better DSL
200
201 Considering how verbose the previous examples were, how would the
202 "ideal" way look like?
203
204 There are different ways which are more, or less intertwined with Java
205 semantics.  On the one end, we could imagine something akin to "Java in
206 Lisp":
207
208 ```
209 (defun read-image (pathname)
210   (ImageIO/read (FileInputStream. pathname)))
211 ```
212
213 Which is almost how it would look like in Clojure.  However, this is
214 complicating semantics.  While importing would be an extension to the
215 package mechanism (or possibly just a file-wide setting), the
216 `Class/field` syntax and `Class.` syntax are non-trivial reader
217 extensions, not from the actual implementation point of view, but from
218 the user point of view.  They'd basically disallow a wide range of
219 formerly legal Lisp names.
220
221 ```
222 (defun read-image (pathname)
223   (#"read" 'ImageIO (new 'FileInputStream pathname)))
224 ```
225
226 This way is the middle ground that we have now.  The one addition here
227 could be that name lookup is done at macro expansion / compilation time,
228 so they are fixed one step before execution, whereas at the moment the
229 JSS reader macro will allow for very late bound name lookup instead.
230
231 The similarity with CLOS would be the use of symbols for class names,
232 but the distinction is still there, since there's not much in terms of
233 integrating CLOS and Java OO yet (which might not be desirable anyway?).
234
235 Auto-coercion to Java data types also takes place in both cases.
236 Generally this would be appropriate, except for places where we'd really
237 want the Java side to receive a Lisp object.  Having a special variable
238 to *disable* conversion might be enough for these purposes.
239
240 If we were to forego the nice properties of JSS by requiring a function
241 form, the following would be another option:
242
243 ```
244 (defun read-image (pathname)
245   $(read 'ImageIO (new 'FileInputStream pathname)))
246 ```
247
248 Where `$(...)` would be special syntax indicating a Java method call.
249 Of course the exact syntax is not very relevant, more importantly static
250 properties could be used to generate a faster, early bound call by
251 examining the supplied arguments as a limited form of type inference.
252
253 ## Summary
254
255 After introducing the necessary steps to start using ABCL with "native"
256 Java libraries, we transcribed two example programs from the library
257 homepage.
258
259 Part of this process was to examine how the interaction between the
260 Common Lisp and Java parts looks like, using the "raw" and the
261 simplified JSS API.  In all cases the FFI is clunkier than needs be.
262 Especially the additional Java namespaces are making things longer than
263 necessary.  The obvious way of "importing" classes by storing a
264 reference in a Lisp variable is viable, but again isn't automated.
265
266 Based on the verbose nature of the Java calls an idea about how a more
267 concise FFI DSL could look like was developed next and discussed.  At a
268 future point in time this idea could now be developed fully and
269 integrated (as a contrib) into ABCL.