bb1c1ca63f924151c73f2064c16260beae36e463
[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 #:jss)
34
35 (use-package '#:jss)
36 ```
37
38 Next, we need access to the right libraries.  After building LIRE from
39 source and executing the `mvn dist` command we end up with a JAR file
40 for LIRE and several dependencies in the `lib` folder.  All of them need
41 to be on the classpath:
42
43 ```
44 (progn
45   (add-to-classpath "~/src/LIRE/dist/lire.jar")
46   (mapc #'add-to-classpath (directory "~/src/LIRE/dist/lib/*.jar")))
47 ```
48
49 ## Prelude
50
51 Since we're going to read pictures in a couple of places, a helper to
52 load one from a pathname is a good start:
53
54 ```
55 (defun read-image (pathname)
56   (#"read" 'javax.imageio.ImageIO (new 'java.io.FileInputStream (namestring pathname))))
57 ```
58
59 To note here is the use of `NEW` from JSS with a symbol for the class
60 name, the conversion of the pathname to a regular string, since the Java
61 side doesn't expect a Lisp object and the `#""` reader syntax from JSS
62 to invoke the method `read` in a bit of a simpler way than using the FFI
63 calls directly.
64
65 Since we can't "import" Java names, we're stuck with either using
66 symbols like this, or caching them in (shorter) variables.
67
68 For comparison, the raw FFI would be a bit more verbose:
69
70 ```
71 (defun read-image (pathname)
72   (jstatic "read" "javax.imageio.ImageIO" (jnew "java.io.FileInputStream" (namestring pathname))))
73 ```
74
75 Though with a combination of JSS and cached lookup it could be nicer,
76 even though the setup is more verbose:
77
78 ```
79 (defvar +image-io+ (jclass "javax.imageio.ImageIO"))
80 (defvar +file-input-stream+ (jclass "java.io.FileInputStream"))
81
82 (defun read-image (pathname)
83   (#"read" +image-io+ (jnew +file-input-stream+ (namestring pathname))))
84 ```
85
86 At this point without other improvements (auto-coercion of pathnames,
87 importing namespaces) it's about as factored as it will be (except
88 moving every single call into its own Lisp wrapper function).
89
90 ## Building an index
91
92 To keep it simple building the index will be done from a list of
93 pathnames in a single step while providing the path of the index as a
94 separate parameter:
95
96 ```
97 (defun build-index (index-name pathnames)
98   (let ((global-document-builder
99           (new 'net.semanticmetadata.lire.builders.GlobalDocumentBuilder
100                (jclass "net.semanticmetadata.lire.imageanalysis.features.global.CEDD")))
101         (index-writer (#"createIndexWriter"
102                        'net.semanticmetadata.lire.utils.LuceneUtils
103                        index-name
104                        +true+
105                        (jfield "net.semanticmetadata.lire.utils.LuceneUtils$AnalyzerType" "WhitespaceAnalyzer"))))
106     (unwind-protect
107          (dolist (pathname pathnames)
108            (let ((pathname (namestring pathname)))
109              (format T "Indexing ~A ..." pathname)
110              (let* ((image (read-image pathname))
111                     (document (#"createDocument" global-document-builder image pathname)))
112                (#"addDocument" index-writer document))
113              (format T " done.~%")))
114       (#"closeWriter" 'net.semanticmetadata.lire.utils.LuceneUtils index-writer))))
115 ```
116
117 The process is simply creating the document builder and index writer,
118 reading all the files one by one and adding them to the index.  There's
119 no error checking at the moment though.
120
121 To note here is that looking up the precise kind of a Java name is a bit
122 of a hassle.  Of course intuition goes a long way, but again, manually
123 figuring out whether a name is a nested class or static/enum field is
124 annoying enough since it involves either repeated calls to `JAPROPOS`,
125 or reading more Java documentation.
126
127 Apart from that, this is mostly a direct transcription.  Unfortunately
128 written this way there's no point in creating a `WITH-OPEN-*` macro to
129 automatically close the writer, however, looking at the `LuceneUtils`
130 source this could be accomplished by directly calling `close` on the
131 writer object instead.
132
133 It would also be nice to have auto conversion using keywords for enum
134 values instead of needing to look up the value manually.
135
136 ## Querying an index
137
138 The other way round, looking up related pictures by passing in an
139 example, is done using an image searcher:
140
141 ```
142 (defun query-index (index-name pathname)
143   (let* ((image (read-image pathname))
144          (index-reader (#"open" 'org.apache.lucene.index.DirectoryReader
145                                 (#"open" 'org.apache.lucene.store.FSDirectory
146                                          (#"get" 'java.nio.file.Paths index-name (jnew-array "java.lang.String" 0))))))
147     (unwind-protect
148          (let* ((image-searcher (new 'GenericFastImageSearcher 30 (jclass "net.semanticmetadata.lire.imageanalysis.features.global.CEDD")))
149                 (hits (#"search" image-searcher image index-reader)))
150            (dotimes (i (#"length" hits))
151              (let ((found-pathname (#"getValues" (#"document" index-reader (#"documentID" hits i))
152                                                  (jfield "net.semanticmetadata.lire.builders.DocumentBuilder"
153                                                          "FIELD_NAME_IDENTIFIER"))))
154                (format T "~F: ~A~%" (#"score" hits i) found-pathname))))
155       (#"closeReader" 'net.semanticmetadata.lire.utils.LuceneUtils index-reader))))
156 ```
157
158 To note here is that the `get` call on `java.nio.file.Paths` took way
159 more time to figure out than should've been necessary:  Essentially the
160 method is using a variable number of arguments, but the FFI doesn't help
161 in any way, so the array (of the correct type!) needs to be set up
162 manually, especially if the number of variable arguments is zero.  This
163 is not obvious at first and also takes unnecessary writing.
164
165 The rest of the code is straightforward again.  At least a common
166 wrapper for the `length` call would be nice, but since the result object
167 doesn't actually implement a collection interface, the point about
168 having better collection iteration is kind of moot here.
169
170 ## A better DSL
171
172 Considering how verbose the previous examples were, how would the
173 "ideal" way look like?
174
175 There are different ways which are more, or less intertwined with Java
176 semantics.  On the one end, we could imagine something akin to "Java in
177 Lisp":
178
179 ```
180 (import '(javax.imageio.ImageIO java.io.FileInputStream))
181
182 (defun read-image (pathname)
183   (ImageIO/read (FileInputStream. pathname)))
184 ```
185
186 Which is almost how it would look like in Clojure.  However, this is
187 complicating semantics.  While importing would be an extension to the
188 package mechanism (or possibly just a file-wide setting), the
189 `Class/field` syntax and `Class.` syntax are non-trivial reader
190 extensions, not from the actual implementation point of view, but from
191 the user point of view.  They'd basically disallow a wide range of
192 formerly legal Lisp names.
193
194 ```
195 (import '(javax.imageio.ImageIO java.io.FileInputStream))
196
197 (defun read-image (pathname)
198   (#"read" 'ImageIO (new 'FileInputStream pathname)))
199 ```
200
201 This way is the middle ground that would be possible.  The one addition
202 to the current JSS system is the importing of Java names and
203 corresponding interaction with the FFI.
204
205 The similarity with CLOS would be the use of symbols for class names,
206 but the distinction is still there, since there's not much in terms of
207 integrating CLOS and Java OO yet (which might not be desirable anyway?).
208
209 Auto-coercion to Java data types also takes place in both cases.
210 Generally this would be appropriate, except for places where we'd really
211 want the Java side to receive a Lisp object.  Having a special variable
212 to *disable* conversion might be enough for these purposes.
213
214 ## Summary
215
216 After introducing the necessary steps to start using ABCL with "native"
217 Java libraries, we transcribed two example programs from the library
218 homepage.
219
220 Part of this process was to examine how the interaction between the
221 Common Lisp and Java parts looks like, using the "raw" and the
222 simplified JSS API.  In all cases the FFI is clunkier than needs be.
223 Especially the additional Java namespaces are making things longer than
224 necessary.  The obvious way of "importing" classes by storing a
225 reference in a Lisp variable is viable, but again isn't automated.
226
227 Based on the verbose nature of the Java calls an idea about how a more
228 concise FFI DSL could look like was developed next and discussed.  At a
229 future point in time this idea could now be developed fully and
230 integrated (as a contrib) into ABCL.