[racket] FFI question again - how to get a string back from C
On 06/11/2011 12:54 AM, keydana at gmx.de wrote:
> Hi again,
>
> sorry for again asking such a basic FFI question, but I have a problem getting an output string from the C side...
>
> E.g. in one case, in my first attempt
>
> (def-ocilib datetotext OCI_DateToText : (date_ptr : _pointer) (fmt : _string) (size : _int) (strval : (_ptr o _string)) -> (result : _bool) -> (values strval result))
>
> I simply tried using (strval : (_ptr o _string)) for the return string (the argument size indicates the desired size for the output string).
>
>
> After the experience with the null-terminated strings from my recent post, I also tried an input-output-arg, passing a null-terminated string in to C:
>
> (def-ocilib datetotext OCI_DateToText : (date_ptr : _pointer) (fmt : _string) (size : _int) (strval : (_ptr io _string) = (gen-output-string 100))-> (result : _bool) -> (values strval result))
>
> where gen-output-string pads an empty string to a specified length.
>
> But in both cases, I get the same error:
>
> ptr-ref: expects type<cpointer> as 1st argument, given: #<bad-value>; other arguments were: #<ctype>
>
> Honestly, I have no idea what the problem might be (or how to debug/investigate it), and would very much appreciate any hints... :-)
I'm not sure if it's possible to solve this problem with (_ptr o ???) or
(_ptr io ???); I'd be very interested to hear if it's possible.
It looks like the function wants you to pass in a buffer for it to write
into. The tricky part seems to be figuring out where the data ends. I
assume the end is indicated by a null terminator. (The APIs I've worked
with have had the courtesy to put the length of the string in an output
parameter, which makes it easier.) Here's how I would do it:
Use _bytes to represent the buffer. A type like _string won't work,
because the C function would be writing to a converted copy of the
string; it wouldn't modify the string you gave it.
The C function writes the formatted date, followed by a null terminator,
to the buffer. Now you need to extract only the formatted date, the part
before the null terminator. You could search for the position of the
null terminator yourself, or you could use an internal Racket function
that makes a byte string ("bytes") from a null-terminated byte array.
(define-ffi-definer define-racket (ffi-lib #f))
(define-racket scheme_make_byte_string
(_fun _bytes -> _racket))
For the main function, if you want to take the buffer as a parameter
(because you calculated the size elsewhere or because you want to reuse
the buffer), define the function this way:
;; OCI_DateToText : date-pointer string bytes -> (values boolean bytes)
(define-ocilib OCI_DateToText
(_fun (date fmt buffer) ::
(date : _pointer)
(fmt : _string)
(size : _int = (bytes-length buffer)
(buffer : _bytes)
-> (result : _boolean)
-> (values result
(scheme_make_byte_string buffer))))
I find it useful to specify the parameters explicitly; that's what the
part before the '::' is. It's sometimes mandatory, especially when
you're calculating some parameter values based on others; I just always
write them out unless the signature is trivial. The part after the first
'->' is a type-spec that specifies what the foreign function returns.
The part after the second '->' is a normal Racket expression that
determines what the Racket wrapper returns.
Note that the definition above gives you the result as bytes; you can
convert it to a string using bytes->string/???, whatever encoding it is.
Or you could use Racket's scheme_make_utf8_string instead of
scheme_make_byte_string if that's the right encoding.
If you want to create the buffer locally:
;; OCI_DateToText : date-pointer string -> (values boolean bytes)
(define-ocilib OCI_DateToText
(_fun (date fmt) ::
(date : _pointer)
(fmt : _string)
(size : _int = 100) ;; FIXME: big enough?
(buffer : _bytes = (make-bytes size))
-> (result : _boolean)
-> (values result
(scheme_make_byte_string buffer))))
HTH,
Ryan