It is also possible to create foreign objects and object types from Scheme, and to access fields of foreign objects from Scheme. For example, the file example from the last section could be equivalently expressed as:
(define-module (my-file) #:use-module (system foreign-object) #:use-module ((oop goops) #:select (make)) #:export (make-file)) (define (finalize-file file) (let ((fd (struct-ref file 0))) (unless (< fd 0) (struct-set! file 0 -1) (close-fdes fd)))) (define <file> (make-foreign-object-type '<file> '(fd) #:finalizer finalize-file)) (define (make-file fd) (make <file> #:fd fd))
Here we see that the result of
make-foreign-object-type, which is
the equivalent of
scm_make_foreign_object_type, is a struct
vtable. See Vtables, for more information. To instantiate the
foreign object, which is really a Guile struct, we use
could have used
make-struct/no-tail, but as an implementation
detail, finalizers are attached in the
initialize method called
make). To access the fields, we use
struct-set!. See Structure Basics.
There is a convenience syntax,
defines a type along with a constructor, and getters for the fields. An
appropriate invocation of
define-foreign-object-type for the
file object type could look like this:
(use-modules (system foreign-object)) (define-foreign-object-type <file> make-file (fd) #:finalizer finalize-file)
This defines the
<file> type with one field, a
constructor, and a getter for the
fd field, bound to
Foreign object types are not only vtables but are actually GOOPS classes, as hinted at above. See GOOPS, for more on Guile’s object-oriented programming system. Thus one can define print and equality methods using GOOPS:
(use-modules (oop goops)) (define-method (write (file <file>) port) ;; Assuming existence of the `fd' getter (format port "#<<file> ~a>" (fd file))) (define-method (equal? (a <file>) (b <file>)) (eqv? (fd a) (fd b)))
One can even sub-class foreign types.
(define-class <named-file> (<file>) (name #:init-keyword #:name #:init-value #f #:accessor name))
The question arises of how to construct these values, given that
make-file returns a plain old
<file> object. It turns out
that you can use the GOOPS construction interface, where every field of
the foreign object has an associated initialization keyword argument.
(define* (my-open-file name #:optional (flags O_RDONLY)) (make <named-file> #:fd (open-fdes name flags) #:name name)) (define-method (write (file <named-file>) port) (format port "#<<file> ~s ~a>" (name file) (fd file)))
As a final note, you might wonder how this system supports encapsulation of sensitive values. First, we have to recognize that some facilities are essentially unsafe and have global scope. For example, in C, the integrity and confidentiality of a part of a program is at the mercy of every other part of that program – because any part of the program can read and write anything in its address space. At the same time, principled access to structured data is organized in C on lexical boundaries; if you don’t expose accessors for your object, you trust other parts of the program not to work around that barrier.
The situation is not dissimilar in Scheme. Although Scheme’s unsafe
constructs are fewer in number than in C, they do exist. The
(system foreign) module can be used to violate confidentiality
and integrity, and shouldn’t be exposed to untrusted code. Although
struct-set! are less unsafe, they still
have a cross-cutting capability of drilling through abstractions.
struct-set! on a foreign object slot could cause
unsafe foreign code to crash. Ultimately, structures in Scheme are
capabilities for abstraction, and not abstractions themselves.
That leaves us with the lexical capabilities, like constructors and
accessors. Here is where encapsulation lies: the practical degree to
which the innards of your foreign objects are exposed is the degree to
which their accessors are lexically available in user code. If you want
to allow users to reference fields of your foreign object, provide them
with a getter. Otherwise you should assume that the only access to your
object may come from your code, which has the relevant authority, or via
code with access to cross-cutting
struct-ref and such, which also
has the cross-cutting authority.