Re: Comparing Pika-syle and JNI-style Tom Lord 14 Jan 2004 21:05 UTC


    > From: Jim Blandy <xxxxxx@redhat.com>

    > Tom Lord <xxxxxx@emf.net> writes:
    > >     > [cadr isn't very interesting, imho -- cadr example snipped]

    > Could you humor me, and post the code anyway?  Of course, feel free to
    > pose other problems.

http://srfi.schemers.org/srfi-50/mail-archive/msg00241.html

    > Well, the code above is meant to be O(1).  Every reference allocated
    > is freed by the next iteration.  Did I miss something?

No, I did -- see http://srfi.schemers.org/srfi-50/mail-archive/msg00297.html

    > > I think it would unduly burden FFI implementors to permit naked
    > > longjmps past SCHEME frames.   Is that a controversial belief?
    > > In other words, suppose that we have a call-chain like:
    > >
    > > 	3RD-PARTY	-- calls setjmp
    > >         SCHEME
    > >         3RD-PARTY	-- calls longjmp?
    > >
    > > In that kind of call chain I think that the longjmp should be declared
    > > illegal by the FFI spec.

    > You're declaring illegal behavior by code neither you or your users
    > control.  But later you suggest a workaround...

Not to that one, no -- no workaround.  There are three kinds of C
functions:

  a) those that don't "call out" to other functions that aren't
     part of their internal definitions

  b) those that _do_ call out to other functions, but which those
     other functions may not assume it is safe to longjmp past

  c) those that call out to other functions but which it _is_ safe
     to longjmp past

For example, if I write a simple binary tree search function that
calls out to some external key-comparison routine -- that's likely to
be in class (c).    On the other hand, if I write a typical GUI event
loop function: that calls out too -- but is more likely to be in class
(b).   And, of course, strchr is in class (a).

An FFI implementation (the "SCHEME" category in the frame
illustrations) is clearly not in (a) because it calls out to functions
which are not part of the FFI implementation.

So should the FFI spec:

	1) require that SCHEME be in (b)
        2) require that SCHEME be in (c)
        3) permit either way (so that portable FFI-using code must
           assume that it is in (b))

My belief -- the one that I asked whether or not it is controversial
-- is that (2) is unacceptable.   It's either (1) or (3) and, in
either case, portable FFI-using code must assume that it is not safe
to longjmp past a SCHEME frame.

    > > but that can be handled by a wrapper too:

    > > 	WRAPPER		-- creates an auxiliary stack
    > >         3RD-PARTY	-- uses setjmp
    > >         FFI-USER*	-- stores frames that might be lost in
    > >                            a longjmp on the auxiliary stack
    > >         3RD-PARTY	-- uses longjmp

    > > which is very similar to what I would expect a JNI/Minor-style
    > > implementation to do but only pays for the costs of the auxiliary
    > > stack when it is actually needed.  This approach is no different from
    > > the kind of thing you'd need to do if, for example, one of the
    > > intermediate FFI-USER* frames wanted to open a temp file that it might
    > > not otherwise get a chance to close.

    > This description isn't clear, but by "auxiliary stack", I assume you
    > mean that the user would actually malloc his frame structures in the
    > "*" frames.  This is, as you suggest, what JNI-style does all the
    > time.

    > The problem here is that you may not know what context code is going
    > to be used in.  And whether a given function is going to get used in
    > such a context is a non-local property.  Pika utility functions that
    > want to make few assumptions about their context must always malloc
    > their frames.

Again, it comes down to the three classes of functions, (a), (b), and
(c).  My proposal allows people to write FFI-using code in any of
those three classes.  If the Pika FFI includes a standard interface to
that auxiliary stack, then people writing FFI-using code in class (c)
get full interoperability with one another.  But in the far more
common case (how many libraries do you know that permit longjmping
past them?), users writing FFI-using code in class (b) don't have to
pay for auxiliary stacks.

    > That's right.  References must be explicitly freed; JNI can help you
    > out in some cases, but you have to think about it.

    > I think that JNI code will often be "linear", in the SRFI-1 sense,
    > with functions like 'f' that accept references being documented to
    > free them.  The "mn_to_car" and "mn_to_cdr" functions are linear
    > variants of "mn_car" and "mn_cdr"; we can add more of these as we find
    > them useful.

Oh dear.   That's the thing: you're winding up not having a common
case of linear functions but instead, having a common case of wanting
two entry points for every function (one linear, one not).   And with
multiple parameters:  should it be linear in all of them?  or just
some?   Sounds like quite a mess.

    > The nice thing about functions that handle references in a linear way
    > is that they are actually faster than ordinary functions: since you're
    > about to free the reference, you know it's not shared amongst any
    > threads, so you can reuse it without memory synchronization.

    > Thus the
    > implementation of mn_to_car:

    >     /* Officially, the following functions deallocate one of the
    >        references they're passed (call it REF), and return a new
    >        reference.  But in fact, they just set REF->obj, and return REF
    >        as the new reference.

    >        This can be done without synchronization, even if REF is a
    >        global reference, because:
    >        - if anyone ever refers to REF assuming the old value, there
    >          must be a race condition, because it's about to be freed, and
    >        - nobody should refer to REF expecting the new value, unless
    >          they received it in some properly-synchronized way, because
    >          it's supposed to be an entirely new reference.  */

    >     mn_ref *
    >     mn_to_car (mn_call *call, mn_ref *ref)
    >     {
    >       mn__begin_incoherent (call);
    >       {
    >         ref->obj = check_pair (ref)->car;
    >       }
    >       mn__end_incoherent (call);
    >
    >       return ref;
    >     }

Isn't that code incorrect in a threaded system?   While `ref' is,
indeed, about to be freed, the pair that it refers to is live.
Assuming that the `incoherent' calls exclude only GC but not other
mutators (which is the benefit you seem to be claiming), then the
`->car' risks producing garbage.

    >    NOTE: Many of the functions in this interface will typically be
    >    used in contexts where the caller "knows" that no error will occur.
    >    Having to check each call to these functions for an exception
    >    return value is a burden; people probably wouldn't do it, and
    >    people's experiences with this interface would be unpleasant.

That's what "(void)" is for? :-)

    >    In the cases where we think this might happen, and where the user
    >    could easily detect the error conditions themselves, we just have
    >    the function abort, rather than returning an exception.  This will
    >    allow errors to be caught earlier.

    > Once I started writing code, I realized that having to check for an
    > error return from every 'car' is going to be an immense amount of
    > clutter.  I was barely willing to even bother, myself.

This is similar to the issue of system calls in unix.  In theory,
nearly all of them need to be checked.   Yet in practice, it's often a
pain in the butt.

The alternative to checking everywhere (other than just aborting the
process) would be to non-locally exit when an error occurs.   So,
fine:  use Pika-style FFI and provide a wrapper function like
`safe_scm_car' which promises never to return an error (but rather to
`scm_longjmp' or `scm_longjmp_exception' instead).

    > > 	err = g (&frame.answer, instance, &frame.x);
    > >         if (err)
    > >           {
    > >             ....;
    > >           }
    > >         err = f (&frame.answer, instance, &frame.answer);
    > >         if (err)
    > >           {
    > >             ...;
    > >           }

    > Right: now imagine that g and f are 'car' and 'cdr'.  What should be
    > 'mn_to_cdr (c, mn_car (c, x))' has become eight lines of code.

Apples and oranges.

The Pika-style equivalent to your code fragment is 2 lines, not 8.

	scm_safely (g (&frame.answer, instance, &frame.x));
	scm_safely (f (&frame.answer, instance, &frame.answer));

The Minor-style equivalent to my code fragment would be at least 8
lines:

	if (some_kind_of_setjmp)
          {
            got an error from g;
          }
        tmp = g (...);
        if (some_kind_of_setjmp)
          {
            got an error from f;
          }
        f (tmp);

    > >     > - Variables are declared normally, and their values used directly.

    > > Variables are declared normally in Pika, too.  I think you mean that
    > > JNI-style attempts to disguise handles as Scheme values.  It is
    > > because it can't pull off that illusion perfectly that I think it is a
    > > questionable choice.

    > There's an illusion at work in Pika, JNI, and SRFI-50, and it oozes
    > out and reveals itself in all three systems.  (SRFI-50's ooze is that
    > it limits when GC can happen.)  What I'm asking is which people
    > consider the least of three oozes.

And I'm not criticizing your for asking.   I admit: Pika-style code is
the ugliest of the lot;  sometimes the most verbose.  I'm just making
the case that that's for very good reasons.

By the way: what is the "illusion" oozing out of Pika-style?  I don't
see it but perhaps I'm just too close to it.

-t