Marc, thank you once again for the excellent explanation. You have a knack for explaining complex topics clearly. We should start collecting and editing this stuff into the Gambit manual proper.
Here's a Unix getgroups() wrapper that allocates increasing powers of two until the result fits. We should finish up SRFI 170 (POSIX APIs) with Harold; since the Unix syscall interface is designed such that the kernel copies data into statically-sized userland buffers, many syscalls are best wrapped by trying powers of two.
The below code raises more potential problems:
- I'm still not confident that I understand when and how to release objects. In the code below, __release_scmobj is used to wrap up accessing ___BODY; is this right?
- getgroups() returns a gid_t array but POSIX doesn't say how many bits wide gid_t is. (In fact, it doesn't even say whether it's a signed or unsigned type.) This is the case for many/most C datatypes. An API like ___alloc_sint_vector(NULL, count, sizeof(gid_t)) would help with this.
- getgroups() can return fewer entries than we allocated buffer space for. Does Gambit have a way of truncating vectors? In general, there is no way to know ahead of time the exact size needed for result buffers: some syscalls accept a NULL buffer to not copy any data and only return the right buffer size, but relying on that size can lead to race conditions between that call and the next call that supplies a non-NULL buffer to actually copy the data.
Clojure transients https://clojure.org/reference/transients might be a good model for Scheme FFI in situations like this. Clojure is mostly an immutable language, but a transient is a mutable collection that is "sealed" into an immutable one once it has been filled; transients were added for performance reasons. While Scheme strings and vectors are mutable, their size cannot change (at least not by RnRS means) so in that respect they are immutable.
Perhaps Gambit's object header can be hacked from C code to mutate a vector to have fewer actual elements than space is allocated for. Is such a thing safe to do, and would it be workable to have an official way in the FFI to truncate a vector?
----------------------------------------------------------------------
(c-declare "#include <unistd.h>")
(define getgroups (c-lambda () scheme-object #<<c-end for (int cap = 2; cap <= 256; cap *= 2) { size_t nbytes = cap * sizeof(___S32); ___SCMOBJ vector = ___EXT(___alloc_scmobj)(NULL, ___sS32VECTOR, nbytes); if (___FIXNUMP(vector)) ___return(___FAL); ___S32 *gids = ___CAST(___S32 *, ___BODY(vector)); int len = getgroups(cap, gids); ___EXT(___release_scmobj)(vector); if (len >= 0) ___return(vector); } ___return(___FAL); c-end ))
(define (writeln x) (write x) (newline))
(let ((gs (getgroups))) (writeln gs) (writeln (s32vector-length gs)))