How are limited API entry points going? Great!

#1

When I first was going about trying to wrangle the ODBC extension to work in Ren-C, it had some C time and date structures that would come back from the database. These are DATE_STRUCT, TIME_STRUCT, and TIMESTAMP_STRUCT. Those have to be turned into TIME! and DATE! values.

But there were no functions in the API yet for making TIME! and DATE! values.

Well, there was rebInteger() for taking a C integer and making an INTEGER!. So to move things along, I went ahead and made functions for taking multiple C integers and making times and dates:

REBVAL *time = rebTimeHMS(hour, minute, second);
REBVAL *date = rebDateYMD(year, month, day);
REBVAL *datetime = rebDateTime(date, time);

But the API vision has established clarity about being of limited entry points. So rather than keep cooking up new C functions taking C arguments, it's better to build on a more limited facility and call Rebol code!

REBVAL *time = rebValue("make time! [",
    rebI(hour), rebI(minute), rebI(second),
"]");

REBVAL *date = rebValue("make date! [",
    rebI(day), rebI(month), rebI(year),
"]");

REBVAL *datetime = rebValue("use [date] [",
    "date:", date,
    "date/time:", time,
"]");

That third one is a little weird, and anyone who wants to suggest a better way of doing it can chime in on issue #2313. But the point is to keep pushing on Rebol the language, not chasing down an infinitely long cascade of API entry points.

I'm feeling really good about it. Though I will re-iterate that it does bring up all those questions of where this stuff is getting bound. You don't want some extension deciding to redefine MAKE, or TIME!, and incidentally wind up breaking the ODBC module's expectations. There has to be some isolation, very much similar to how modules will be isolated...except (somehow) applying to the C code.

But that's the vision, and if you haven't been reading the interesting usages like in the ZeroMQ extension, please do! Would be good to hear any thoughts people are having, while watching it materialize...

1 Like
#2

In the "strings are going great" topic...

I mentioned my concerns about Red's C APIs like redAppend(). How would you know what refinements to make parameters? Is there a redAppendPart(), a redAppendPartOnly(), etc?

Ren-C went with a fully generic string-plus-splicing interface. Every call invokes the evaluator, and so the only difference between the entry points is based on what kind of result you want...so that you can get atomic C values without worrying about separate steps to pick apart API values.

And if you just want to ignore the result entirely, there's rebElide()...so...

 rebElide("append/dup", block, value, rebI(num));

But this raises big questions. Not only where would these strings be bound, but what would it do to performance to have to bind strings every call? What about the added cost of path dispatch and word lookup to invoke a function? An explicit API entry point for rebAppendDup() could be orders of magnitude faster.

I waved my hands a bit and we discussed options which would be a bit like prepared statements. But we already have this technology today:

 REBVAL *append_dup = rebValue(":append/dup");

 void rebAppendDup(REBVAL *series, REBVAL *value, REBVAL *count)
     { rebElide(append_dup, series, value, count); }

Boom. You specialize the function once in startup, and store it in a global somewhere, release it on shutdown. And now during the call you have no loading or binding, or path dispatch. It doesn't need to create a BLOCK! or series to hold these variadic parameters, the evaluator feeds them straight from the C va_list! It's reading the C stack directly.

We could make this even easier with a helper that marked a value as something to auto-free on shutdown. let's imagine rebPrepare() does that:

 void rebAppendDup(REBVAL *series, REBVAL *value, REBVAL *count) {
      static REBVAL *append_dup; // defaults to null pointer
      rebPrepare(&append_dup, ":append/dup"); // only update append_dup if null

      rebElide(append_dup, series, value, count);
 }

rebPrepare() can even be an inline function, so the pointer test of append_dup to null wouldn't need to make an extern "C" function call except the first time.

With a bit of additional cleverness, this could probably just be:

 void rebAppendDup(REBVAL *series, REBVAL *value, REBVAL *count) {
      static REBVAL *append_dup;
      rebElide(rebP(&append_dup, ":append/dup"), series, value, count);
 }

To sum up: Winning! :zap:

2 Likes