From 0ac93751d0054c1ba1e86d71031b41e94e073a6f Mon Sep 17 00:00:00 2001 From: =?utf8?q?Adri=C3=A1n=20Arroyo=20Calle?= Date: Mon, 27 Feb 2023 20:02:21 +0100 Subject: [PATCH] FFI: Documentation --- src/ffi.rs | 21 ++++++++++ src/lib/ffi.pl | 101 +++++++++++++++++++++++++++++-------------------- 2 files changed, 82 insertions(+), 40 deletions(-) diff --git a/src/ffi.rs b/src/ffi.rs index 2910a20d..afc52339 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -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}; diff --git a/src/lib/ffi.pl b/src/lib/ffi.pl index c34cb4ad..cce60ff6 100644 --- a/src/lib/ffi.pl +++ b/src/lib/ffi.pl @@ -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 -- 2.54.0