From: Hoehle, Joerg-C. <Joe...@t-...> - 2003-03-05 15:04:56
|
Dear reader, [Sam: this text will hopefully answer questions regarding FOREIGN-VALUE et = al.] the text below assumes some familiarity with the CLISP FFI declarations and reading of prior FFI cookbook texts of mine (i.e. sent on 12th of February 2002 to this list). How to deal with variable length (unknown at compile time) arrays with the CLISP FFI? The aforementioned cookbook part already contains an example for gethostname. It was relatively easy, since the maximal array length (MAXHOSTNAMELEN: 256) is a system-wide constant. There's a well-known variable length case everybody knows about: C-STRING. You may not have recognized that C-ARRAY-PTR is a generalization of C-STRING to any other type: (C-ARRAY-PTR CHARACTER) is equivalent to C-STRING - except for efficiency. Yet that is still not enough to handle typical needs. I'll use zlib's compress function as an example throughout this document. Its declaration is: int compress (Bytef *dest, uLongf *destLen, const Bytef *source, uLong sourceLen); However that does not tell enough for a FFI definition. The documentation defines: "sourceLen is the byte length of the source buffer. Upon entry, destLen is the total size of the destination buffer, which must be at least 0.1% larger than sourceLen plus 12 bytes. Upon exit, destLen is the actual size of the compressed buffer." Now this is all we need to know about compress. Here is what I dream of: (def-lib-call-out zlib-compress [eventually library] (:name "compress") (:arguments (dest (ffi:c-ptr (ffi:c-array ffi:uint8 is_len(destlen))) :out :guard (zerop return)) (destlen ffi:ulong :in-out) (source (ffi:c-ptr (ffi:c-array ffi:uint8 is_len(sourcelen)))= ) (sourcelen ffi:ulong)) (:return-type ffi:int)) Well, the CLISP FFI does not support this kind of declaration. We need to do it by hand. We will need a Lisp wrapper around the foreign function. I'll distinguish input from output (or :in-out) cases and start with :in parameter declarations. Part 1: input parameters (:IN) source is a typical :in parameter. It's a variable length array, and its length is passed as an extra argument. If, in our application, we're going to compress strings only, then the above declaration could be changed to (c-ptr (c-array character is_len(sourcelen))) -- at least in what I dream of. But in such cases, I'd rather just use C-STRING. The difference is a terminating NUL (0) character that the FFI will supply automatically. The callee (zlib) will not even see it, since it is not allowed to read more than sourcelen characters. The equivalent declaration for bytes instead of a string is: (C-ARRAY-PTR UINT8) (def-lib-call-out zlib-compress [eventually library] (:name "compress") (:arguments (dest ... :out #|:guard (zerop return)|#) (destlen ulong :in-out) (source (c-array-ptr uint8)) (sourcelen ulong)) (:return-type ffi:int)) (defun my-compress (sourcebytes) "Accepts an array of bytes and returns a vector of its compressed content= s." (let ((sourcelen (length sourcebytes))) (multiple-value-bind (status actual) (zlib-compress <destlen> bytes sourcelen) ...))) Several people have inquired whether CLISP would stop at a 0 byte occurring within sourcebytes. The answer is no. From the Lisp point of view, a whole array is passed trough the interface. Contents don't matter, therefore the length is not limited by an eventual (POSITION 0 sourcebytes). Summary: Do waste a terminating 0 byte (or int, or NULL pointer) by using C-STRING or (C-ARRAY-PTR x) when applicable. This has much less overhead and easier to read than the variable length support that we cannot circumvent for output parameters. However, C-STRING or C-ARRAY-PTR are not useable in call cases. We then have to use a technique as developed below for the :OUT (or :in-out) case. Part 2: output parameters (:OUT or :IN-OUT) compress' dest is a typical output parameter. One solution path would be to construct a FFI definition at run-time, when we know the actual source and dest buffer length. We would need to weight the time needed to build, parse and compile such a definition vs. the gain from being able to use an exact array type declaration comprising the size. (defun my-compress (source) (let* ((sourcelen (length source)) (destlen (+ 12 (ceiling (* sourcelen 1.05))))) (funcall (foreign-address-function=09=09; not yet in CLISP #'zlib-compress `(c-function ... (dest (C-ARRAY CHARACTER ,destlen) :out) (destlen (C-PTR ulong) :in-out) (source (C-ARRAY CHARACTER ,sourcelen)) (sourcelen ulong))) destlen source sourcelen))) This is one way to go, which in the case of compress is not enough, because only part of the dest buffer is filled in, as returned in <destlen> on output. We could use SUBSEQ on the result of the call to zlib-compress, but that would be some waste of storage. Instead, let us allocate the destination buffer on the stack, using FFI:WITH-FOREIGN-OBJECT. This is faster and makes more sense than using malloc() (via FFI:SIMPLE-CALLOC or FFI:DEEP-MALLOC) and UNWIND-PROTECT. We must then pass a C-POINTER (a FOREIGN-ADDRESS object) to this buffer to the foreign function. (def-lib-call-out zlib-compress [eventually library] (:name "compress") (:arguments (dest C-POINTER :IN #|:guard (zerop return)|#) (destlen ulong :in-out) (source (c-array-ptr uint8)) (sourcelen ulong)) (:return-type ffi:int)) Note how the former :OUT declaration for dest now turns into an :IN declaration. That's why C doesn't know about :in or :out, as opposed to other languages. All it sees is pointers. (defun my-compress (sourcebytes) "Accepts an array of bytes and returns a vector of its compressed content= s." (let* ((sourcelen (length source)) (destlen (+ 12 (ceiling (* sourcelen 1.05))))) (WITH-FOREIGN-OBJECT (dest 'uint8 :count destlen) (multiple-value-bind (status actual) (zlib-compress (FOREIGN-ADDRESS dest) destlen sourcebytes sourcel= en) (if (zerop status) ...))))) Actually, I haven't yet implemented the :COUNT extension to WITH-FOREIGN-OBJECT (so far it's only available with SIMPLE-CALLOC and DEEP-MALLOC). Let's use an equivalent (but less efficient) form instead: (WITH-FOREIGN-OBJECT (dest `(c-array uint8 ,destlen)) Note how I passed (FOREIGN-ADDRESS dest) instead of `dest' to the foreign function. The reason is that WITH-FOREIGN-OBJECT creates an object of type FOREIGN-VARIABLE, which one can understand as encapsulating a FOREIGN-ADDRESS object together with a foreign type. I'm thinking about submitting a patch to src/foreign.d which would made this call superfluous: a C-POINTER declaration would be satisfied by either untyped FOREIGN-ADDRESS (as by now) or typed FOREIGN-VARIABLE objects. What did we achieve so far? We managed to pass to the foreign function a buffer exactly as large as needed, along with its size. The foreign function will fill part of it, and we need to extract the results. The dest buffer is only filled when the function was successful: (if (zerop status) (subseq (FOREIGN-VALUE dest) 0 actual) (error ...)) The code above dereferences the whole buffer, then takes out the part that was actually filled in, allocating and copying another sequence. We shall do without it. The CLISP FFI contains macros to operate on what is called foreign places. In the future, it will export their lower-level functional counterparts which operate on FOREIGN-VARIABLE objects, as obtained by e.g. with-foreign-object. These functions shall have a trailing * in their name, e.g. ELEMENT* instead of ELEMENT, etc. (It's undecided whether I should rename FOREIGN-VALUE to FOREIGN-VALUE* for coherence, or only rename in case of name duplication -- please tell me). Remember the foreign type of dest: (C-ARRAY UINT8 <destlen>) What we want is a shorter array. Let's use CAST*, like any C programmer. (foreign-value (CAST* dest `(C-ARRAY UINT8 ,actual))) ; broken Using CAST* here does not work, though, because CLISP insists on keeping the size of the foreign structure constant when casting. So we are going to use OFFSET* instead, which lets us conceptually overlay an address or memory range with another structure. (if (zerop status) (foreign-value (FFI:OFFSET* dest 0 `(C-ARRAY UINT8 ,actual))) to be compared with the original form: (subseq (FOREIGN-VALUE dest) 0 actual) Joining all steps, the complete example becomes: (defun my-compress (sourcebytes) "Accepts an array of bytes and returns a vector of its compressed content= s." (let* ((sourcelen (length source)) (destlen (+ 12 (ceiling (* sourcelen 1.05))))) (WITH-FOREIGN-OBJECT (dest 'uint8 :count destlen) (multiple-value-bind (status actual) (zlib-compress (FOREIGN-ADDRESS dest) destlen sourcebytes sourcel= en) =09(if (zerop status) =09 (FOREIGN-VALUE (OFFSET* dest 0 `(C-ARRAY UINT8 ,actual))) =09 (error "zlib::compress error code ~D" status)))))) However, as of CLISP-2.30, these names with the trailing star are not yet exported from the package FFI. They're named FFI::%CAST instead, which is not engaging. Instead of using these, I'll show how to write equivalent code using what CLISP calls foreign places. A foreign place is a concept similar to that of a generalized variable. Instead of using with-foreign-object, we shall write WITH-C-VAR. The code looks almost the same. (WITH-C-VAR (dest `(c-array uint8 ,destlen)) (multiple-value-bind (status destbytes actual) =09(zlib-compress (C-VAR-ADDRESS dest) destlen sourcebytes sourcelen) (if (zerop status) =09(subseq dest 0 actual) =09(error "zlib::compress error code ~D" status)))) `Dest' now denotes a foreign place. From a technical point of this means no more than there is a SYMBOL-MACROLET which wraps its uses into a FOREIGN-VALUE form. Therefore, evaluating (reading) `dest' dereferences the foreign memory's contents. Setting it (using SETQ or SETF) writes to foreign memory. The address of this foreign places can be obtained with C-VAR-ADDRESS -- FOREIGN-ADDRESS would not work. So one saves typing FOREIGN-VALUE. What else is there about it? On the positive side, it combines nicely with Lisp. Foreign structures can be used smoothly. And it works with old CLISPs (OFFSET has been there since day one of the CLISP FFI). Please compare the resulting code with the above. (defun my-compress (sourcebytes) "Accepts an array of bytes and returns a vector of its compressed content= s." (let* ((sourcelen (length source)) (destlen (+ 12 (ceiling (* sourcelen 1.05))))) (WITH-C-VAR (dest 'uint8 :count destlen) (multiple-value-bind (status actual) (zlib-compress (C-VAR-ADDRESS dest) destlen sourcebytes sourcelen= ) =09(if (zerop status) =09 (OFFSET dest 0 `(c-array uint8 ,actual)) =09 (error "zlib::compress error code ~D" status)))))) On the negative side, I find the automated dereferencing dangerous, since the programmer must be careful about each of its appearances: usually only within other FFI forms like SLOT, ELEMENT, CAST, OFFSET. In particular, it should not be passed to another function: this passes the dereferenced value, not a reference to foreign memory! As an example, consider returning from a function the buffer and its size: (values dest (length dest))=09;BROKEN This code reads the foreign buffer twice and builds two Lisp vectors out of it!. One should have used instead: (values dest (SIZEOF dest)) or (let ((dest dest)) (values dest (length dest))) Let ((dest dest)) is a little bit obfuscated: the Lisp variable could (or should) have been renamed instead, giving: (let ((dest-as-Lisp-vector dest)) (values dest-as-Lisp-vector (length dest-as-Lisp-vector))) Furthermore, using FOREIGN-VARIABLE objects instead of foreign places feels closer to programming with references or proxies (or objects?) as when using Java, Python or C++ etc. Last but not least: an object of type FOREIGN-VARIABLE can be stored anywhere and passed to a function, like every other object. Foreign places cannot. It's natural for the functions SIMPLE-CALLOC and DEEP-MALLOC to return such objects. I've been considering adding a WITH-FOREIGN-PLACE macro: it would define a foreign place out of a foreign-variable (or foreign address) object. This would be useful in those portions of code which access slots of the foreign structure. (WITH-FOREIGN-PLACE (place (gethash key table (DEEP-MALLOC ...))) (element place 0)) WITH-C-VAR now appears as a combination of it and of WITH-FOREIGN-OBJECT: (with-foreign-object (-var- type [initform]) (WITH-FOREIGN-PLACE (place -var-) ...body)) (defmacro WITH-FOREIGN-PLACE ((place foreign-variable) &body body) (let ((fv (gensym (symbol-name place)))) `(let ((,fv ,foreign-variable)) (symbol-macrolet ((,place (FOREIGN-VALUE ,fv))) ,@body)))) =09 Lifting the 1:1 encoding restriction on strings Western people tend to forget about it, but custom:*DEFAULT-FOREIGN-ENCODING* comes into play every time C-STRING or CHARACTER declarations are involved. How to deal with e.g. UTF-8, UTF-16, Japaneese or Corean encodings? One trivial - yet successful - solution path would be to use a composition of known elements: (defun compress-string-to-bytes (string encoding &key (start 0) (end nil)) (my-compress (ext:convert-string-to-bytes string encoding :start start :end end))) We shall investigate another solution. It involves WITH-FOREIGN-STRING. This form is specialized in allocating strings on the execution stack and accepts encoding, start and end arguments. WITH-FOREIGN-STRING yields a FOREIGN-ADDRESS object (not a foreign variable as with-foreign-object) that we shall pass as the source to the foreign function. We thus need another FFI declaration: (def-lib-call-out zlib-compress [eventually library] (:name "compress") (:arguments (dest C-POINTER :in #|:guard (zerop return)|#) (destlen ulong :in-out) (source C-POINTER) (sourcelen ulong)) (:return-type ffi:int) (:language :stdc)) The WITH-FOREIGN-STRING macro takes a program body and binds three variables to a FOREIGN-ADDRESS object pointing to the converted string, its original length in elements (modulo the NULL-TERMINATED-P key) and its length in bytes. I designed it not to use a FOREIGN-VARIABLE object because: 1. most uses would be to pass its address to a foreign function anyway, 2. more importantly, its unclear what the proper foreign type should be. One may think that for fixed-with encodings like e.g. UTF-16, it should result in (C-ARRAY UINT16 <element-count>) but that would imply that the programmer might not be in control of the type since the actual encoding may be defined by the user. Therefore, only (C-ARRAY UINT8 <sourcelen>) seems to make sense. But wouldn't (C-ARRAY-MAX UINT8 <sourcelen>) not be more appropriate? So I decided to stick with an untyped address. Should you have a need to access individual characters, then you can "cast" it to a foreign variable object of the type that you need using FOREIGN-ADDRESS-VARIABLE (not yet in CLISP). (FOREIGN-ADDRESS-VARIABLE dest `(c-array uint8 ,sourcelen)) I'm thinking about using the already existing CAST macro or CAST* function for this purpose. Then you can use typical accessors on it: (ELEMENT* (foreign-address-variable dest `(c-array uint 8 ,sourcelen)) 0) would extract the first byte (if length allows). Nevertheless, the code below invokes FOREIGN-ADDRESS on dest, just so to feel safe should I eve change my mind. Given a FOREIGN-ADDRESS object, it's in effect an identity function. (defun compress-string-to-bytes (string encoding &key (start 0) (end nil)) "Return a vector of compressed bytes from STRING, according to ENCODING" (WITH-FOREIGN-STRING (source element-count sourcelen =09=09=09string =09=09=09:null-terminated-p nil =09=09=09:encoding encoding =09=09=09:start start :end end) (declare (ignore element-count)) (let ((destlen (+ 12 (ceiling (* sourcelen 1.05))))) (with-c-var (dest 'uint8 :count destlen) (multiple-value-bind (status actual) (zlib-compress (c-var-address dest) destlen =09=09=09 (FOREIGN-ADDRESS source) sourcelen) =09 (if (zerop status) =09 (offset dest 0 `(c-array uint8 ,actual)) =09 (error "zlib::compress error code ~D" status))))))) =09 Efficiency considerations I did not perform actual measurements, yet with the current API, it is quite probable that using SUBSEQ is faster than using CAST or OFFSET etc. The reason is that in the latter case, a lot is happening at run-time in CLISP: 1. create a (c-array uint8 <actual> list using backquote 2. transform it into the internal type representation 3. construct a FOREIGN-VARIABLE with this type 4. dereference memory using FOREIGN-VALUE It therefore becomes questionable whether the advantage of dereferencing only <actual> bytes instead of the whole <destlen> number of bytes and not creating an extra vector is worth our effort! It appears that what stands in our way towards performance is the extra level of abstraction introduced by the CLISP FFI. What we see is some kind of interpretation overhead. I'm not speaking about a byte-code interpreter, but about an interpreter specialized on manipulating foreign variable descriptions and interpreting forms like OFFSET, ELEMENT, SLOT etc. With a Lisp-to-native code compiler like CMUCL, clever compiler optimizations and a good FFI API, this run-time overhead could be avoided. W.r.t. to the API, a small problem here is the use of backquote, which means that the compiler would have to figure out that the backquoted list only serves to build an external type description for a variable length array. It should be easier to express and understand this, for both the programmer and the compiler: that's why I introduced the :count keyword to the memory allocating functions. It's easier to optimize (DEEP-MALLOC 'uint8 :count (foo x)) than (DEEP-MALLOC `(c-array uint8 ,(foo x))) By contrast, the Amiga-CLISP AFFI has no such interpretation level. The API that it provides is the equivalent of machine level: the work that in all cases must be done is memory transfer, the rest is overhead. Therefore, the AFFI only contains 4 functions. A lot can be implemented on top of these. There is one function for foreign function call and three for memory transfer: MEM-READ, MEM-WRITE and MEM-WRITE-VECTOR. Using AFFI, we would have written: (let ((result (MAKE-ARRAY actual :element-type '(unsigned-byte 8)))) (MEM-READ (foreign-address dest) result)) It would have been really fast, with the least possible overhead, but the programmer would have to write the code in an imperative style, which to the compiler theory purist (like me) appears like applying partial evaluation or compiler optimization techniques by hand, which s/he considers a shame. Yet working with the AFFI does not let one feel stranger than working with C, or with any of the other Lisp's FFIs. It's working with a lot of low-level stuff like buffer allocation, correct buffer size etc. which declarative style (like CLISP's DEF-CALL-OUT) tends to avoid. Yet when DEF-CALL-OUT is not enough and a wrapper is needed, using CLISP's DEREF, WITH-FOREIGN-OBJECT etc. does not feel like programming at a highler level than using other FFIs. Compared to these, the overhead of all the intermediate FOREIGN-VARIABLE objects and steps is too high IMHO. IMHO, an API which produces code which will be order of magnitudes slower than its C counterpart will rightly be the target of criticisms. =09=09Declarative style wins Remember what I said I dream of? (def-lib-call-out zlib-compress [eventually library] (:name "compress") (:arguments (dest (ffi:c-ptr (ffi:c-array ffi:uint8 is_len(destlen))) :out :guard (zerop return)) (destlen ffi:ulong :in-out) (source (ffi:c-ptr (ffi:c-array ffi:uint8 is_len(sourcelen)))= ) (sourcelen ffi:ulong)) (:return-type ffi:int)) Isn't that much simpler than all this cumbersome and error-prone low-level handling with buffers, memory, arrays, their sizes, etc.? CLISP's declarative DEF-CALL-OUT already manages to provide a FFI definition for many many foreign functions. I believe something like the above is likely to provide 80% of the remaining needs. Feel free to investigate defining such a form. If you do, consider making it powerful enough to handle Postgres' string quoting function: http://cert.uni-stuttgart.de/doc/postgresql/escape/ size_t PQescapeString (char *to, const char *from, size_t length); "The from points to the first character of the string which is to be escaped, and the length parameter counts the number of characters in this string (a terminating NUL character is neither necessary nor counted). to shall point to a buffer which is able to hold at least one more character than twice the value of length, otherwise the behavior is undefined. A call to PQescapeString writes an escaped version of the from string to the to buffer, replacing special characters so that they cannot cause any harm, and adding a terminating NUL character. PQescapeString returns the number of characters written to to, not including the terminating NUL character. Behavior is undefined when the to and from strings overlap." One may argue that its parameter usage is badly designed, if not braindead. The difficulty I see here in finding a way to declaratively express the required interface is that the size of the to buffer and the number of useful bytes come from to different places (1+ (* length 2)) vs. the return value, as opposed to compress' destlen :in-out parameter. Writing wrappers by hand on a case by case basis maybe error-prone, but it is straightforward. It doesn't require much brain. =09=09Summary o By all means, use C-STRING or (C-ARRAY-PTR type) whereever possible. o Foreign places provide a somewhat elegant, at least compact, however inefficient way of working on foreign structures. o The required DEF-CALL-OUT parameter declarations depend on the wrapper. There's no "one declaration for all uses" as in C. o A worthwhile approach instead of writing ad hoc code which every time loooks similar (with-foreign-object etc.) would be to provide a DEF-VARLEN-CALL-OUT macro which would encapsulate all this with-foreign-object, foreign-value, subseq etc. code. (def-lib-call-out zlib-compress [eventually library] (:name "compress") (:arguments (dest C-POINTER :in #|:guard (zerop return)|#) (destlen ulong :in-out) (source (c-array-ptr uint8)) (sourcelen ulong)) (:return-type ffi:int) (:language :stdc)) (defun my-compress (sourcebytes) "Accepts an array of bytes and returns a vector of its compressed content= s." (let* ((sourcelen (length source)) (destlen (+ 12 (ceiling (* sourcelen 1.05))))) (WITH-C-VAR (dest uint8 :count destlen)) (multiple-value-bind (status actual) (zlib-compress (C-VAR-ADDRESS dest) destlen sourcebytes sourcelen= ) =09(if (zerop status) =09 (OFFSET dest 0 `(C-ARRAY UINT8 ,actual)) =09 (error "zlib::compress error code ~D" status)))))) Things not (yet) in CLISP: o DEF-CALL-VAR-OUT macro for variable length arrays o :count extension to WITH-FOREIGN-OBJECT and WITH-C-VAR o rename OFFSET* SLOT* ELEMENT* etc. instead of %OFFSET %SLOT o exporting FOREIGN-VALUE and OFFSET* etc. from FFI o export and document FFI:PARSE-C-TYPE and its converse o FOREIGN-ADDRESS-FUNCTION (so far only part of my dynload patch) o dynamic library call out (dynload patch to be completed) o FOREIGN-ADDRESS-VARIABLE (if not reusing CAST) o possibly make CALL-OUT code accept FOREIGN-VARIABLE objects where type C-POINTER is expected (so far only FOREIGN-ADDRESS) o WITH-FOREIGN-PLACE macro o compile-time or load-time inlining of constant PARSE-C-TYPE use as in (deep-malloc 'uint8 :size (foo x)) or with-foreign-object ... o ... I appreciate comments, =09J=F6rg H=F6hle. |