Changes to LD-handling in library code

Hi there, folks! I’ve got another big refactor in the works, and I want to give y’all a heads-up on it. This ONLY affects people doing work on the C code of libswipl itself; it does NOT at all affect:

  • Writing Prolog code, regardless of if it’s part of the swipl distribution
  • Writing packs/extensions that get loaded into swipl at runtime
  • Writing external applications that use the public swipl API to embed Prolog

If you fall only into the above categories in your relationship with SWI-Prolog, you can feel free to ignore the rest of this email! If not, you might want to read on.

With that out of the way; this affects the variable called LD, which stores all of the per-thread or per-engine persistent state: the “Local Data” structure. At present, there are a number of ways to access it:

  1. Use the macro GLOBAL_LD, which retrieves an always-accessible pointer to the structure, but might need to call inefficient functions to retrieve it.
  2. Use LOCAL_LD, which assumes the pointer has already been retrieved into the current function.
  3. Use LD, which is defined as a synonym for either GLOBAL_LD or LOCAL_LD above, depending on the file.
    If this function is using LOCAL_LD, then its definition needs to come from somewhere:
    1. GET_LD in the body of the function, or PRED_LD in the body of a native predicate implementation.
    2. ARG_LD at the end of a function prototypes parameter list, and a corresponding PASS_LD at the end of the callsite arguments list.
    3. ARG1_LD as the single parameter in a function declaration, with a corresponding PASS_LD1 as the single argument.
    4. Using a macro defined in pl-ldpass.h, which appends PASS_LD or PASS_LD1 as appropriate to the function you’re calling; you are still responsible for providing a local LD in that function if this is in LOCAL_LD mode.

It can get confusing. Hence: refactor.

The basic high-level idea is that there should only be two types of functions inside libswipl: those that never need to reference LD in any way, either in their own code or in functions they call, and functions that get passed LD as an argument. Effectively, this means that LOCAL_LD is in effect for all library-internal code, and GET_LD/PRED_LD will never need to be used. The one exception is when control passes into the library from external code; if the function call doesn’t provide LD (or a way to uniquely identify the LD in question) and it does need access to it, GET_LD fetches the variable from TLS in a stub function before passing it to the real implementation.

To this end, I’m adding two major new macros that people will need to be familiar with: LDFUNC (with its associated DECL_LD and LDFUNC_DECLARATIONS) and API_STUB. These combine to effectively make all internal libswipl functions operate in mode 3.4 above; LD never needs to get passed explicitly, as long as your functions are properly declared.

LDFUNC is used in the #define directives that make LD-passing macros, and they all take exactly the same form:

#define PL_copy_term_ref(from) LDFUNC(PL_copy_term_ref, from)

This takes care both of mangling the function name (so that the internal function referenced as PL_copy_term_ref won’t conflict with the external API function PL_copy_term_ref) and ensuring that LD is declared and passed as appropriate. Every library-internal function that deals with LD will have one of these #define macros associated with them, so that you’ll never have to think about whether the function you’re calling needs a PASS_LD or not. The #define lives either with the header prototype, if it’s a library-internal function, with the forward declaration of a static function at the top of a file, or with the function definition itself, if it’s static and doesn’t need a forward declaration. That way there shouldn’t ever be confusion about whether a function has an LD-passing shorthand or not; they all do.

Declaring the passed-LD argument for such a function takes one of two forms. For standalone prototypes, like in a header file or a block of static forward declarations, you can surround the list of prototypes with a define/undef for the macro LDFUNC_DECLARATIONS, and in that case the function prototypes themselves don’t need any modification, or indeed any mention of LD at all:

#define LDFUNC_DECLARATIONS
#define PL_copy_term_ref(from) LDFUNC(PL_copy_term_ref, from)
term_t  PL_copy_term_ref(term_t from);
#undef LDFUNC_DECLARATIONS

Or you can put the macro DECL_LD inside the function parentheses, right at the start before the first argument (if there is one), with no comma. It’s basically just like ARG_LD, with two exceptions: it goes at the beginning, not the end, and you don’t have to vary ARG_LD vs ARG1_LD.

#define PL_copy_term_ref(from) LDFUNC(PL_copy_term_ref, from)
term_t  PL_copy_term_ref(DECL_LD term_t from);

term_t
PL_copy_term_ref(DECL_LD term_t from)
{ /* code goes here */ }

As you can see, it works for prototypes or for definitions, and semantically it has exactly the same effect as the LDFUNC_DECLARATIONS method.

Point of interest: I have changed from passing it in the last argument to passing it in the first argument. It’ll probably save a few cycles here and there, in the case that one LD-using function calls exactly one other LD-using function and doesn’t need to use LD on its own, because (for any architecture with a register-passing call convention) the argument will already be in the proper register on function arrival, since it’s always in the same position. It also makes vararg functions easier to deal with, since you always know which argument is in the last named position :slight_smile:

API_STUB, in contrast, is someone easier to understand. It’s used when defining libswipl API functions that need to use LD, and the syntax (with an LDFUNC definition and prototype for context) looks as follows:

#define PL_copy_term_ref(from) LDFUNC(PL_copy_term_ref, from)
term_t  PL_copy_term_ref(DECL_LD term_t from);

API_STUB(term_t)
(PL_copy_term_ref)(term_t from)
( return PL_copy_term_ref(from); )

API_STUB provides two things: first, it avoids expanding the PL_copy_term_ref(from) macro in the declaration, by enclosing the function name inside its own parentheses; that means the name won’t get mangled with a suffix. Second, it prepends a GET_LD to the function body (which is why the function body is in parentheses, not braces), so you don’t have to worry about where your function-local LD is coming from. The fact that the function body is in parentheses does mean we won’t get line number debug information for the function, but hopefully these stubs will all be simple and short anyway.

You’ll note that this function is using PL_copy_term_ref(from) in its code; since API_STUB protects against function name expansion, there’s no need for an #undef/#define pair to write the declaration. In point of fact, the API_STUB macro isn’t even needed for that—C is perfectly happy to have a function name in parentheses in a function declaration, and that also stops the function-like macro from expanding. I really like having the function name in parentheses in this case, actually, because it’s a hint that “something weird is going on with the function declaration here”, which should hopefully make it look a little bit less like an infinite self-recursion at first glance.

This has been a lot! Hopefully for the one or two people this actually impacts, it makes enough sense. You can see an initial commit, making changes to pl-fli.c and pl-fli.h:

And obviously, I’m interested to hear any thoughts folks have!

2 Likes

All this is way above my head … but could you provide a bit of context, as a learning opportunity:

What exactly is stored in the local data structure?

This is a fantastic question! However, despite how much of the library code I’m touching, I think I’m probably actually not the best person to answer it, and I’m also interested in an expert answer :slightly_smiling_face: @jan?

All things that are thread/engine specific. When introducing multi threading all the good old global variables where categorized into three classes:

  • Stuff that is initialized once, shared and never changes
  • Stuff that is shared between threads and changes. That is the hard bit as we need to synchronize the changes.
  • Stuff that is specific to a thread (stacks, thread-local tabling, current I/O streams, thread-local flags, etc).

The latter two categories are each in a giant C struct, one called GD (global data) and one LD (local data). The first is just a global variable. The latter is accessible as thread local data. As the access to thread local data through pthread_getspecific() is a lot slower than passing it around as argument most functions pass LD around. For the single threaded version LD can be allocated statically as GD, so there is a bunch of macros to switch between the models. @dmchurch knows way more C preprocessor tricks than I do and came up with something that hides the LD passing a lot better.