CAPTCHA, the simplest gst external module
This is a translation of which is a blog post in Japanese by Hiroshi Higa. I don't speak Japanese, so I asked the author to write a very simple summary of the article, which I then augmented.
Basically, his idea is to implement a captcha. There are good captcha libraries for Squeak, e.g. SW2Captcha which uses Morphic to generate images. That may sound like using a bazooka to shoot an ant, but it's actually quite nice and the code is interesting. In GNU Smalltalk, to do the same you would need GD bindings — note to self, put that on the todo list!
Hiroshi thought that he just wanted to tech himself a bit of FFI, so he would do a bigger part of the job in C. This approach may be less extensible, less OO, less everything, but it also provides a very basic but pratical example of FFI, and made his post simple and easy to understand.
In GNU Smalltalk there are basically two ways to do dynamic linking: libraries and modules. With libraries, every time you create an FFI call the system checks every library you added for an implementation of the function. With modules, the module itself tells GNU Smalltalk what functions it provides.
Hiroshi's module is basically a single C function, plus the code to tell GNU Smalltalk about that function's existence:
#include <stdio.h> #include "gstpub.h" #include "gd.h" static VMProxy *vmProxy; int imageString(char *string, char *fontname, unsigned char *fg, unsigned char *bg, double ptsize, char *filename) { gdImagePtr image; int background, foreground; int brect[8]; int x, y; char *err; FILE *out; err = gdImageStringFT(NULL, &brect[0], 0, fontname, ptsize, 0.0, 0, 0, string); if (err) {return 1;} x = brect[2] - brect[6] + 6; y = brect[3] - brect[7] + 6; image = gdImageCreate(x, y); background = gdImageColorResolve(image, bg[0], bg[1], bg[2]); foreground = gdImageColorResolve(image, fg[0], fg[1], fg[2]); x = 3 - brect[6]; y = 3 - brect[7]; err = gdImageStringFT(image, &brect[0], foreground, fontname, ptsize, 0.0, x, y, string); if (err) {return 1;} out = fopen(filename, "wb"); gdImagePng(image, out); fclose(out); gdImageDestroy(image); return 0; } void gst_initModule(VMProxy * proxy) { vmProxy = proxy; vmProxy->defineCFunc("imageString", imageString); }
In more complex examples, you would use
vmProxy to access and create Smalltalk objects. But FFI allows you to pass Smalltalk objects as C types in a very natural way.
Compiling the module is somewhat a mess. If you want to do it portable you do it with libtool (see Creating and distributing packages on the wiki for more info) but for Linux you just need this:
gcc -shared -Wl,-soname,gd.so.1 -lgd -lpng -lz -ljpeg \ -lfreetype -ldl -lm -o gd.so gd.o
Put the file in your LD_LIBRARY_PATH or under /usr/lib/smalltalk. Or you can leave it in the current directory and run GNU Smalltalk with
SMALLTALK_MODULES=. gst
Here is the Smalltalk side of the module:
Object subclass: GD [ GD class >> imageString: aString font: fontPath foreground: fg background: bg size: size to: file [ <cCall: 'imageString' returning: #int args: #(#string #string #byteArray #byteArray #double #string)> ] ]
And here is how you test your module. First, we load the module and file in the package. You can also make a .star file and a nice package following the instructions in the wiki link given above:
DLD addModule: 'GD'. FileStream fileIn: 'GST_DIR/share/smalltalk/GD/GD.st'. "A useful method... (will be in 3.0.1)" SequenceableCollection extend [ atRandom [ ^self at: (Random between: 1 and: self size) ] ] "Make a four character captcha." fontPath := '/usr/share/fonts/bitstream-vera/Vera.ttf'. authChars := '0123456789'. authChars := authChars, 'abcdefghijklmnopqrstuvwxyz'. authChars := authChars, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'. authString := String streamContents: [ :s |. 4 timesRepeat: [ s nextPut: authChars atRandom ]]. GD imageString: authString font: fontPath foreground: #[255 255 255] background: #[0 0 0] size: 40 to: 'captcha.png'.
That's it.
I see a couple of mistakes:
- The definition of the method in class GD seems to be missing the "cCall ..." part (oh, I see, it also disappeared from my comment).
- The method is defined as an instance method but the example sends it to the class.
- The first parameter to imageString should be "authString".
Thanks for the example, I'll be back with a million questions :D
Thanks, I addressed all your comments.