non-local exits are icky Tom Lord (23 Dec 2003 23:07 UTC)
Re: non-local exits are icky Michael Sperber (26 Dec 2003 15:41 UTC)
Re: non-local exits are icky Tom Lord (26 Dec 2003 18:14 UTC)
Re: non-local exits are icky Michael Sperber (26 Dec 2003 18:30 UTC)
Re: non-local exits are icky Tom Lord (26 Dec 2003 19:19 UTC)
Re: non-local exits are icky Michael Sperber (27 Dec 2003 16:16 UTC)
Re: non-local exits are icky Tom Lord (27 Dec 2003 18:38 UTC)
Re: non-local exits are icky Michael Sperber (27 Dec 2003 18:43 UTC)

Re: non-local exits are icky Tom Lord 27 Dec 2003 19:03 UTC


    T>>> [The FFI lacks unwind-protection for C code which is a
    T>>>  serious problem since the FFI also uses non-local exits past
    T>>>  C code.  I see a choice in the design space: either add
    T>>>  unwind protection for C, or user error-codes rather than
    T>>>  than non-local exits.  I think the latter is cleaner.]

I'll also add that they're equivalently expressive: given an
error-code-using FFI I can write a layered FFI that uses
non-local-exits-past-C and provides unwind-protection for C;
conversely, given non-local exits and unwind-protection for C, I can
write a layered FFI that uses error codes.

Scheme's dynamic-wind (your proposed answer) plus the proposed FFI
comes _close_ to providing unwind protection for C, but falls short.
I can not layer on top of it either an error-code based interface, or
an interface that provides sufficient unwind protection from C.

In general, let's say we want to write a C function which is
_conceptually_:

  fn ()
  {
    declare_my_local_state;

    init_my_local_state;  /* cleanups required after this */

    do_some_scheme_stuff;  /* might trigger a non-local exit */

    cleanup_my_local_state;
  }

You're saying, let's write that conceptual function as four actual C
functions:

  fn_prolog ()
  {
    allocate_my_local_state_on_the_scheme_heap;
    init_my_local_state_part_1;
  }

  fn_postlog ()
  {
    cleanup_my_local_state_part_2;
  }

  fn_body ()
  {
    init_my_local_state_part_2;
    do_some_scheme_stuff;
    cleanup_my_local_state_part_1;
  }

  fn ()
  {
    arrange_to_scheme_eval (dynamic-wind fn_prolog fn_body fn_postlog);
  }

The biggest problem with that solution is that it can not be adapted
for use with libaries not written in anticipation of it.  Here's an
illustration:

A common C idiom, one you can likely find in just about any GUI
toolkit for example, is the "callback" idiom.  A library exports entry
points like:

	add_callback_fn (event_type_t event_type, int (*)(fn));

        handle_event (event_t event);

where calling handle_event can result in calls to a bunch of functions
registered with add_callback_fn.  A callback mechanism can reasonably
make provisions for errors: a callback function may return an error
code causing the callback run to be aborted or to otherwise change its
course.

An FFI should be able to handle a situation where:

	Scheme calls out to C, eventually to handle_event.

        handle_event calls to C, to my_hook_fn.

        my_hook_fn calls out to Scheme

Unless the GUI library I'm using has made it safe to longjmp past
handle_event, I can not implement this using the proposed FFI.  A
Scheme error underneat my_hook_fn will want to exit all the way past
handle_event.   There is no way, upon such an error, to return flow
control back to handle_event.

Another problem concerns the awkwardness of protecting against
asynchronous interrupts.  While we can reasonably expect interrupts to
be implicitly masked during the prolog and postlog, they are possible
from the time the prolog returns to the time the postlog is entered.
Meanwhile, the body is required to manage the local state in such a
way that the postlog can always clean up reliably -- which means it
will sometimes have to make complex atomic updates to that state.

In the general case, then, one will have to write
SCHEME_CALL_SANS_INTERRUPTS essentially as a call to

  (dynamic-wind noop noop atomic-step)

and use that in fn_body for every update to the local state -- you'll
need not just the four functions above, but a bunch of additional
helper functions for those updates.  I despair of many programmers
either wanting to write such code or of getting it right.

Another very minor problem is that the FFI provides no reliable means
by which to apply a version of DYNAMIC-WIND.  Either a C function,
SCHEME_DYNAMIC_WIND is needed, or it should be defined as a mandatory
shared binding.

    > ... and handle the non-local control flow from Scheme.  C just
    > isn't powerful enough to play these games gracefully.

Well, yes, that's my point.

As I see it, the FFI design-space has a bifurcation wrt to non-local
exits from Scheme past C code:

1) You can not use them -- use error values instead.  C is quite
   powerful enough to handle those.  They're easy to understand and,
   most importantly, they "play nicely" with more C libraries.
   They tend to be lower-overhead in the common case where errors
   don't occur.   As I recall, Tcl (which has a notoriously clean and
   easy to use FFI) goes this route.

2) You can try to design an unwind-protect mechanism for C and add
   that to the FFI.  This is what Guile and Systas have done
   internally for years and if you look at recent guile-dev list
   traffic, you can see that they're working on adding a similar
   facility to their "simpler, easier to use" FFI layer (the gh_
   layer).

-t