Re: Comparing Pika-syle and JNI-style Jim Blandy 14 Jan 2004 19:53 UTC

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.

>     >     mn_ref *
>     >     assq (mn_call *c, mn_ref *key, mn_ref *alist)
>     >     {
>     >       while (mn_pair_p (c, alist))
>     >         {
>     >           mn_ref *pair = mn_car (c, alist);
>     >           mn_ref *pair_key = mn_car (c, pair);
>     >
>     >           if (mn_ref_eq (c, key, pair_key))
>     >             return pair;
>     >
>     >           mn_free_local_ref (c, pair);
>     >           mn_free_local_ref (c, pair_key);
>     >           alist = mn_to_cdr (c, alist);
>     >         }
>     >
>     >       return mn_false (c);
>     >     }
>
> Note, by the way that while my assq is arguably uglier than yours,
> mine is O(1) space and yours is O(N) where N is the length of the
> alist.  (Does that count as a benefit of having more hair on my assq?
> (sorry :-)).

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

I think that's a very serious issue, and I tried to mention both
halves of it in my point-by-point comparison.

>     > 				[...] longjmp is even harder.
>
> It's not that hard -- you just can't use "naked" longjmp to jump past
> protected frames (you would have to use a `scm_setjmp' / `scm_longjmp'
> pair.)

Yes, that would work.

> 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...

> 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.

>     > - Variable declarations are cluttered with enclosing structs and GCPRO
>     >   / UNGCPRO calls.
>
> Tastes vary.   In isolation, at least, that seems a weak reason to use
> a more costly approach.

That's why it's last.  And the JNI-style list includes a "clutter"
bullet, too.

>     > - Functions may only return Scheme values by reference; they may not
>     >   provide them as their (syntactic) return values.  Instead of writing
>     >   "f (g (x))", you must write:
>
>     >     g (&frame.x, &frame.temp);
>     >     f (&frame.temp, &frame.temp2);
>
>     >   In other words, you must write your code as linear series of
>     >   operations which work by side-effects.
>
>     > In JNI-style:
>     > - Functions can return references directly, so code need not be
>     >   linearized.  You can write "f (call, g (call, x))" --- if you know
>     >   that "call" will return and free g's return value soon enough.
>     > - Local references are freed automatically when the Scheme->C call to
>     >   which they belong returns.  Leaks due to unfreed local references
>     >   (which will probably be the most common sort of error) have a
>     >   bounded and often (though not always) short lifetime.
>
>
> One thing that bothers me about the JNI-style:
>
> 	f (g(x), h(y))
>
> is the lifetime of the references returned from `g' and `h'.  If that
> call is in a loop, for example, two new references will be allocated
> on every iteration and, absent the programmer taking steps to
> explicitly manage them (sacrificing the gain of the function call
> syntax) all of those references remain live until C returns back to
> Scheme.

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.

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;
    }

> As a side effect of that, functions which should consume O(1) space
> will, in basic JNI-style, consume O(N) where N is the number of
> intermediate results they create during computation.

Only if the code is written naively.  I suggested assq as an example
for exactly this reason; my assq is O(1), unless I'm missing
something.

> This also comes back to error handling.  We haven't talked about it
> much yet but my thinking is that nearly every FFI function should be
> able to return an error code and nearly every call to an FFI function
> should be checked.  For example, if `scm_car' is passed a non-pair
> argument, I think it should be able to return an error code.

I started out that way, but then I got rid of it.  From minor/minor.h:

   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.

   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.

> 	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.

>     > - 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.