Pros and cons of a tight coehesion typed FFI

Guess nobody got my Prolog VM suggestion. Here it is again in its full glory:

It would also be possible to add a rich FFI to SWI-Prolog. Currently SWI-Prolog emits the following Prolog VM code for a poor foreign function stub, where <func> is the function pointer. The code is emitted depending on arity, no type information is used:

I_FCALLDET0 <func>   /* arity 0 */
I_FCALLDET1 <func>   /* arity 1 */ 
I_FCALLDET2 <func>   /* arity 2 */
Etc..

Lets assume we have now a foreign function alarm(integer) . What can be generated as rich foreign function stub. In as far the Albufeira Prolog interpreter is not unsuitable, having adopted the approach from the paper is not really an obstacle. New instructions ???1??? and ???2???:

???1??? some guard to check whether the first argument is integer
???2??? some conversion to convert the Prolog tagged pointer
               into foreign integer
I_FCALLDET1 alarm 

The problem is probably garbage collection etc… since suddently foreign data type arguments are on the stack window. I don’t know how to solve this problem in the case of SWI-Prolog, because I don’t know enough about SWI-Prolog.

So yes, its non trivial somehow. Not sure. Also I_FCALLDETX has hard coded that the arguments are terms with call-by-reference, passing consecutive addresses. A rich FFI has usually also call-by-value, and not only call-by-reference.

The Prolog VM compiler would not only generate a Prologue for each FFI, depending on the types it knows. The Prolog would also generate an Epilogue ffor each FFI to fetch the result. There are various theoretical advantages of the approach:

  • One could inline the Prolog VM sequences when a predicate is called, so that the supervisor overhead is not needed.
  • Fresh variable analysis can then automatically switch between costly unify for a result or cheaper alternatives.

This is because the conversion is not handled by a generator inside the foreign language. The foreign language has no “Prolog knowledge”. It doesn’t know anything about fresh variables etc…

On the other hand a Prolog VM compiler is expert in these matters. A foreign language can also do some of these optimization, but possibly only at runtime and not at compile time.

So usualy such a FFI can give up to 2x times speed-up or so.

1 Like

I am learning much from your postings.

While its obvious, i didn’t connect in my mind that it VM instructions that handle foreign interface calls.

From my little understanding i am wondering if it makes sense to have a dedicated VM instruction that dedicated for deterministic foreign calls – and have a designated subset of FFI for such calls – hopefully, simplifying calls, foreign function writing, and call handing a lot – including perhaps a-priori designation of what are input and what are output variables.

And, then have VM instructions that are non-determinstic – the prolog way – that then also puts additional demands on the foreign predicate implementor.

Dan

Concerning the TYPE_XXX you want to support, you might or might
not get inspiration from ISO core standard:

Edit 07.05.2021:
A typical optimization a compiler can do, is omitting conversion
completely. Assume you have two predicates:

:- foreign(p/1, p(-integer)).
:- foreign(q/1, p(+integer)).

And now you have some code:

r :-  ..., p(X), q(X), ...

Usally the generated code would be:

I_FCALLDET1 p
????3??? some Epilog of p
????4??? some Prologue for q
I_FCALLDET1 q

This could be merged into, provided the variable X is fresh,
because p will give back an integer and q will take an integer:

I_FCALLDET1 p
I_FCALLDET1 q

I don’t quite understand why this would have to be a new set of virtual machine instructions. Why not do these checks on the other side, in the compiled function that prolog is calling? That’s where all the context is of what the function is actually going to do with these terms, so that is the place with all the information, where a compiler can determine best how to handle such issues as garbage collection too.

Of course that means it doesn’t just work with a plain c function but it doesn’t have to. You can add an extra compile step in here. This is not uncommon - an example that immediately comes to mind is QT which extends plain c++ with signals and slots, requiring a preprocessing step to properly make that work. And in a language like rust, it can all be done through macros.

1 Like

Constructing a C call from a typed array of arguments is far from trivial. There are a couple if libraries to that (one used by the SWI-Prolog ffi pack). The ffi pack combines an array of types with an array of term_t types (which you get for free anyway in SWI-Prolog). Besides the portability limitations involved in this approach I stand by the reasons not going into the direction I gave in the post linked below.

I like what I see in the Rust interface!

P.s. The current approach also avoids most of the GC issues. For example, we can pass the native C string associated with an atom as the term_t argument protects the atom from being garbage collected while the foreign code runs.

Because the Prolog VM is expert on Prolog terms. And it could do optimizations:

The foreign functions cannot do that. If you have a Prolog code as in the example, i.e. a query p(X), q(X), and if you go trough a FFI, most compilers will give up. A compiler might optimize inside the foreign language statements sequences of the form:

var X = p();
p(X);

But if you stitch together a FFI call p/1 and a FFI call q/1 inside Prolog, there is no way for a compiler to help you. The Prolog VM compiler needs to be smart.