FFI: Documentation

This commit is contained in:
Adrián Arroyo Calle
2023-02-27 20:02:21 +01:00
parent f94294dbd9
commit 0ac93751d0
2 changed files with 82 additions and 40 deletions

View File

@@ -1,3 +1,24 @@
/* How does FFI work?
Each WAM machine has a ForeignFunctionTable instance that contains a table of functions and structs.
Structs are defined via foreign_struct/2. Basic types are defined by libffi, but struct types need to
be manually defined to get an ffi_type. Additionally, to recover structs from return arguments, we store
fields and atom_fields, as a way to lookup the content of the struct (fields) and the nested structs (atom_fields).
Functions are defined via use_foreign_module/2. It opens a library and leaks the memory of the library,
to prevent Rust freeing the memory. There's no way to recover that memory at the moment. We get a pointer for
each function and we build a CIF for each one, with the input arguments and the return argument.
Exec happens via '$foreign_call', we find the function, we try to cast the values that we have to the definition
of the function, we reserve memory for them and we build an array of pointers. To get the return argument, we
reserve enough memory for the return and we build the Scryer values from them.
Structs are a bit tricky as they need to be aligned. For that, we reserve enough memory (libffi calculates that)
and for each field: we add to the pointer until we're aligned to the next data type we're going to write, we write it,
and finally we add the pointer the size of what we've written.
*/
use crate::atom_table::Atom;
use std::alloc::{alloc, Layout};

View File

@@ -1,8 +1,65 @@
:- module(ffi, [use_foreign_module/2, foreign_struct/2]).
/** Foreign Function Interface
This module contains predicates used to call native code (exposed by the C ABI).
It uses [libffi](https://sourceware.org/libffi/) under the hood. The bridge is very simple
and is very unsafe and should be used with care. FFI isn't the only way to communicate with
the outside world in Prolog: sockets, pipes and HTTP may be good enough for your use case.
The main predicate is `use_foreign_module/2`. It takes a library name (which depending on the
operating system could be a `.so`, `.dylib` or `.dll` file). and a list of functions. Each
function is defined by its name, a list of the type of the arguments, and the return argument.
Types available are: `sint8`, `uint8`, `sint16`, `uint16`, `sint32`, `uint32`, `sint64`,
`uint64`, `f32`, `f64`, `cstr`, `void`, `bool`, `ptr` and custom structs, which can be defined
with `foreign_struct/2`.
After that, each function on the lists maps to a predicate created in the ffi module which
are used to call the native code.
The predicate takes the functor name after the function name. Then, the arguments are the input
arguments followed by a return argument. However, functions with return type `void` or `bool`
don't have that return argument. Predicates with `void` always succeed and `bool` predicates depend
on the return value on the native side.
```
ffi:FUNCTION_NAME(+InputArg1, ..., +InputArgN, -ReturnArg). % for all return types except void and bool
ffi:FUNCTION_NAME(+InputArg1, ..., +InputArgN). % for void and bool
```
## Example
For example, let's see how to define a function from the [raylib](https://www.raylib.com/) library.
```
?- use_foreign_module("./libraylib.so", ['InitWindow'([sint32, sint32, cstr], void)]).
```
This creates a `'InitWindow'` predicate under the ffi module. Now, we can call it:
```
?- ffi:'InitWindow'(800, 600, "Scryer Prolog + Raylib").
```
And a new window should pop up!
*/
:- use_module(library(lists)).
:- use_module(library(error)).
%% foreign_struct(+Name, +Elements).
%
% Defines a new struct type with name Name, composed of the elements Elements, which is a list
% of other types.
%
% The name of the types doesn't matter, but the order of Elements must match the ones in the
% native code.
%
% Example:
%
% ```
% ?- foreign_struct(color, [uint8, uint8, uint8, uint8]).
% ```
foreign_struct(Name, Elements) :-
'$define_foreign_struct'(Name, Elements).
@@ -16,10 +73,9 @@ assert_predicate(PredicateDefinition) :-
functor(Head, Name, NumInputs),
term_variables(Head, TermList),
Body = (
lists:maplist(ffi:check_input, Inputs, TermList),
'$foreign_call'(Name, TermList, _),!
),
Predicate =.. [:-, Head, Body],
Predicate = (Head:-Body),
assertz(ffi:Predicate).
assert_predicate(PredicateDefinition) :-
@@ -28,10 +84,9 @@ assert_predicate(PredicateDefinition) :-
functor(Head, Name, NumInputs),
term_variables(Head, TermList),
Body = (
lists:maplist(ffi:check_input, Inputs, TermList),
'$foreign_call'(Name, TermList, 1),!
),
Predicate =.. [:-, Head, Body],
Predicate = (Head:-Body),
assertz(ffi:Predicate).
assert_predicate(PredicateDefinition) :-
@@ -43,41 +98,7 @@ assert_predicate(PredicateDefinition) :-
term_variables(Head, TermList),
Body = (
lists:append(TermListInputs, [TermListReturn], TermList),
lists:maplist(ffi:check_input, Inputs, TermListInputs),
'$foreign_call'(Name, TermListInputs, TermListReturn),!
),
Predicate =.. [:-, Head, Body],
Predicate = (Head:-Body),
assertz(ffi:Predicate).
check_input(sint8, Var) :-
must_be(integer, Var),
(
(Var > -129, Var < 128) ->
true
; domain_error(integer_does_not_fit, Var, foreign_call/3)
).
check_input(sint16, Var) :-
must_be(integer, Var),
(
(Var > -32769, Var < 32768) ->
true
; domain_error(integer_does_not_fit, Var, foreign_call/3)
).
check_input(sint32, Var) :-
must_be(integer, Var),
(
(Var > -2147483649, Var < 2147483648) ->
true
; domain_error(integer_does_not_fit, Var, foreign_call/3)
).
check_input(sint64, Var) :-
must_be(integer, Var).
check_input(f32, _Var).
check_input(f64, _Var).
check_input(cstr, Var) :-
must_be(chars, Var).
check_input(_, Var).
% must_be(list, Var).
% TODO: assert native predicates.
% They MUST validate types