- Notifications
You must be signed in to change notification settings - Fork0
Lean4 Tutorial/Notes on creating FFI bindings with GLFW as an example.
License
DSLstandard/Lean4-FFI-Programming-Tutorial-GLFW
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
There seems to be no complete tutorial on creating C FFI bindings in Lean4. So I am writing notes here. I am putting the keyword "tutorial" in the GitHub project name just so people who search "Lean4 FFI tutorial" will land on this and hopefully get some guidances, but by no means does this markdown serve as a complete, 100% correct tutorial. I don't even know what is going on half of the time when reading the sources:
src/include/lean/lean.h
inhttps://github.com/leanprover/lean4- https://lean-lang.org/lean4/doc/dev/ffi.html (it seems to be outdated as it uses a keyword called
constant
, which does not actually exist in Lean4). - https://github.com/leanprover/lean4/tree/master/src/lake/examples/ffi
(This section might need you to have some understanding in writing C FFIs in Haskell, or writing language bindings in general. Otherwise you might have no idea what is going on or why are things the way they are.)
Say you would like to write some C FFI bindings in Lean4 tolibglfw.so
, a popular C library for creating windows and OpenGL contexts. Let's also say you plan to create Lean4 bindings toglfwInit
,glfwTerminate
,glfwPollEvents
,glfwCreateWindow
andglfwWindowShouldClose
. In fact, I picked these examples because:
glfwInit
,glfwTerminate
, andglfwPollEvents
are simple functions, great for an introduction to writing Lean4 bindings.glfwCreateWindow
involves passing an opaque object from C to Lean4. I will also demonstrate how to passString
s from Lean4 to C.glfwWindowShouldClose
involves passing an opaque object from Lean4 to C.- They provide the minimal set of functionalities to create a working GLFW window.
In short, you will basically have to: (You can skip this paragraph if you want and move on to the steps to creating FFIs for GLFW right now)
- Write a
.c
source file providing the right functions and function signatures that compiled Lean4 programs know how to talk to. - Modify your
lakefile.lean
to...- compile the
.c
file to produce a static lib, - and add the necessary compilation/linking flags to link
libglfw.so
and the static lib of the.c
to your Lean4 application executable whenever you do$ lake build
.
- compile the
- Write
opaque
Lean4 definitions in a.lean
module in your Lean4 project that point to the certain functions written in that.c
file. You might also have to define some opaque structures if the FFI functions return pointers to, say, a C handle object.
For reference, here are the functions you would like to bind to:
// Initializes the GLFW library.// `GLFW_TRUE` if successful, or `GLFW_FALSE` if an error occurred.intglfwInit(void);// Terminates the GLFW library.voidglfwTerminate(void);// Creates a GLFW window,// and returns an abstract handle to it.GLFWwindow*glfwCreateWindow(intwidth,intheight,constchar*title,// window titleGLFWmonitor*monitor,// you may ignore this argumentGLFWwindow*share// you may ignore this argument);// See: https://www.glfw.org/docs/latest/group__window.html#ga37bd57223967b4211d60ca1a0bf3c832voidglfwPollEvents(void);// Did the user intend to close `window`? (e.g., pressing the [X] button)intglfwWindowShouldClose(GLFWwindow*window);
Here is a basic GLFW example that creates a blank GLFW window and ends when the user closes the GLFW window:
#include<GLFW/glfw3.h>#include<stdlib.h>intmain() {if (!glfwInit()) {perror("Cannot initialize GLFW!!\n");returnEXIT_FAILURE; }GLFWwindow*win=glfwCreateWindow(800,600,"My GLFW window's title",NULL,NULL);while (!glfwWindowShouldClose(win)) {glfwPollEvents(); }glfwTerminate();printf("Goodbye.\n");}
Our goal will be to rewrite this program in "pure Lean4".
Suppose in the beginning, you did$ lake init
and get the following:
.├── lakefile.lean├── lake-manifest.json├── lean-toolchain├── Main.lean├── <Your main library directory>/├── <Your main library name>.lean└── ... possibly more files
First, define the followingopaque
functions in a.lean
file in your Lean4 project (I will explain the details later). For simplicity, I will write them in./Main.lean
.
-- There are no need for more additional imports.@[extern "lean_glfwInit"]opaque glfwInit : IO Bool@[extern "lean_glfwTerminate"]opaque glfwTerminate : IO Unit@[extern "lean_glfwPollEvents"]opaque glfwPollEvents : IO Unit-- An opaque type to represent `GLFWwindow *`opaque WindowP : NonemptyTypedef Window := WindowP.type@[extern "lean_glfwCreateWindow"]opaque glfwCreateWindow (width : UInt32) (height : UInt32) (title : String) : IO Window-- We will ignore `monitor` and `share`.@[extern "lean_glfwWindowShouldClose"]opaque glfwWindowShouldClose (win : Window) : IO Booldef main : IO Unit := do IO.println "Hello world!"
Next, create a file called./native.c
(the name is arbitrary) and put the following:
#include<lean/lean.h>// IMPORTANT!!lean_obj_reslean_glfwInit(lean_obj_argworld) {// TODO:}lean_obj_reslean_glfwTerminate(lean_obj_argworld) {// TODO:}lean_obj_reslean_glfwCreateWindow(uint32_twidth,uint32_theight,b_lean_obj_argtitle,lean_obj_argworld) {// TODO:}lean_obj_reslean_glfwWindowShouldClose(lean_obj_argwinp,lean_obj_argworld) {// TODO:}lean_obj_reslean_glfwPollEvents(lean_obj_argworld) {// TODO:}
After that, modify your./lakefile.lean
to automatically compile./native.c
and link the generated static lib andlibglfw.so
to your Lean4 application executable for future$ lake build
calls. Here is an example:
import Lakeopen Lake DSLpackage «YourProjectName» where -- add package configuration options here@[default_target]lean_exe «glfw_test_exe» where root := `Main moreLinkArgs := #["-lglfw", "-Wl,--allow-shlib-undefined"] -- To link `libglfw.so` to your Lean4 app exe.-- Create target named `native.o` by compile `./native.c`-- Honestly, this is kind of like writing Makefilestarget native.o (pkg : NPackage _package.name) : FilePath := do let native_src := "native.c" let native_c := pkg.dir / native_src let native_o := pkg.buildDir / "native.o" buildFileAfterDep native_o (<- inputFile native_c) fun native_src => do let lean_dir := (<- getLeanIncludeDir).toString compileO "My native.o compilation" native_o native_src #["-I", lean_dir, "-fPIC"]-- Create a static lib from `native.o`extern_lib native (pkg : NPackage _package.name) := do let name := nameToStaticLib "native" let native_o <- fetch <| pkg.target ``native.o buildStaticLib (pkg.buildDir / "lib" / name) #[native_o]
What is going on with./native.c
and those@[extern "XXX"]
opaque definitions in our./Main.lean
?
In./Main.lean
:
- Each
@[extern <func_name>]
marked opaque function in./Main.lean
corresponds to a function with the name<func_name>
defined in./native.c
. - The
WindowP
andWindow
part in./Main.lean
will become apparent later when we start passingGLFWwindow *
objects around between Lean4 and C. I don't have an explanation as to why it is written like this, so just copy it and change the names for other opaque C types.
Now, let's take a look at./native.c
. We will first inspectlean_glfwInit
:
// Lean4's counterpart in ./Main.lean:// @[extern "lean_glfwInit"]// opaque glfwInit : IO Boollean_obj_reslean_glfwInit(lean_obj_argworld) {// TODO: we will call glfwInit() here and return the initialization status code back as a Bool}
lean_glfwInit
corresponds toopaque glfwInit : IO Bool
in our./Main.lean
as indicated by us marking the Lean4 function@[extern "lean_glfwInit"]
. Whenever we callopaque glfwInit : IO Bool
in our Lean4 program,lean_glfwInit
from./native.c
will be called.
Inlean_glfwInit
's function signature, you can see that the function takes in onelean_obj_arg
and returns alean_obj_res
. These are Lean4 objects, the typedefs come from#include <lean/lean.h>
.
- Side note: When
lake
compiles./native.c
,#include <lean/lean.h>
will point to the filesrc/include/lean/lean.h
as defined inhttps://github.com/leanprover/lean4/blob/master/src/include/lean/lean.h.
In fact,lean_obj_arg
andlean_obj_res
are just aliases to the typelean_object *
. Here are their definitions ripped out fromlean/lean.h
:
/* The following typedef's are used to document the calling convention for the primitives. */typedeflean_object*lean_obj_arg;/* Standard object argument. */typedeflean_object*b_lean_obj_arg;/* Borrowed object argument. */typedeflean_object*u_lean_obj_arg;/* Unique (aka non shared) object argument. */typedeflean_object*lean_obj_res;/* Standard object result. */typedeflean_object*b_lean_obj_res;/* Borrowed object result. */
and here islean_object
's definition if you are curious:
typedefstruct {intm_rc;unsignedm_cs_sz:16;unsignedm_other:8;unsignedm_tag:8;}lean_object;
Do be informed that Lean4's usages oflean_object
andlean_object *
are non-trivial. There are some tricks likelean_object *
pointer tagging for constants and other black magics in play for optimization. I will NOT touch on them. However, you can find all the disgusting details in the comments written onhttps://github.com/leanprover/lean4/blob/master/src/include/lean/lean.h (The whole header only has around 2000 lines).
Before explaining more onlean_glfwInit
's actual function signature, observe the following translations between Lean4 function types and C function types:
A function that takes inno arguments and returnsUnit
.
// @[extern "foo1"]// opaque foo1 : Unitlean_obj_resfoo1();// [!] Returning a `Unit` requires returning a `lean_obj_res`.
A function that takes in aUnit
and returnsUnit
.
// @[extern "foo2"]// opaque foo2 : (a : Unit) -> Unitlean_obj_resfoo2(lean_obj_arga);// [!] Taking in 1 argument.
...and now 3Unit
s as input.
// @[extern "foo3"]// opaque foo3 : (a : Unit) -> (b : Unit) -> (c : Unit) -> Unitlean_obj_resfoo3(lean_obj_arga,lean_obj_argb,lean_obj_argc);// [!] Taking in 3 arguments.
Here is a function with "special Lean4 types" (uint8_t
,uint32_t
).
// @[extern "foo4"]// opaque foo4 : (a : UInt8) -> (b : UInt32) -> (c : Unit) -> Unitlean_obj_resfoo4(uint8_ta,uint32_tb,lean_obj_argc);// [!] Certain Lean4 types have special translations// Please see https://lean-lang.org/lean4/doc/dev/ffi.html#translating-types-from-lean-to-c for a complete(?) list.// Here, UInt8 maps to uint8_t, and UInt32 maps to uint32_t.
..and here is one that takes in aBool
, which is mapped touint8_t
. It is also a "special Lean4 type".
// @[extern "foo5"]// opaque foo5 : (mybool : Bool) -> Unitlean_obj_resfoo5(uint8_tmybool);// [!] Bool maps to uint8_t, where 0 is false and 1 is true.// See https://lean-lang.org/lean4/doc/dev/ffi.html#translating-types-from-lean-to-c.
Here is a function that takes inuint8_t
(a special Lean4 type) and returnsuint16_t
(another special Lean4 type). Notice that we are not returning alean_obj_res
, but auint16_t
. (TODO: cite an official reference on this behaviour, I learnt this through experimenting with FFIs and see what segfaults.)
// @[extern "foo6"]// opaque foo6 : (mybool : Bool) -> UInt16uint16_tfoo6(uint8_tmybool);// [!] Returning a uint16_t instead of the usual `lean_obj_res` here because UInt16 translates to uint16_t.
foo7
is the most important example. It does I/O.
// @[extern "foo7"]// opaque foo7 : (mybool : Bool) -> IO Unitlean_obj_resfoo7(uint8_tmybool,lean_obj_argworld);// [!] Functions with the return type `IO` are quite interesting,// Lean4 expects them to take in an extra lean_obj_arg,// and they *MUST* return a lean_obj_res.// =======================================================================================// I have not been able to find an official explanation for this seemingly strange function signature.// But here is my guess://// We have:// opaque foo7 : (mybool : Bool) -> IO Unit//// Consider that, in Lean4:// abbrev IO : Type → Type := EIO Error// def EIO (ε : Type) : Type → Type := EStateM ε IO.RealWorld// def IO.RealWorld : Type := Unit// def EStateM (ε σ α : Type u) := σ → Result ε σ α// inductive Result (ε σ α : Type u) where// | ok : α → σ → Result ε σ α// | error : ε → σ → Result ε σ α//// This means that:// IO a// = EIO Error a// = EStateM Error IO.RealWorld a// = IO.RealWorld -> Result Error IO.RealWorld a// = Unit -> Result Error Unit a (IO.RealWorld is just Unit)//// Therefore,// foo7 : (mybool : Bool) -> IO Unit// = foo7 : (mybool : Bool) -> IO.RealWorld -> Result Error IO.RealWorld Unit// = foo7 : (mybool : Bool) -> (world : Unit) -> Result Error Unit Unit// ^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^// first arg second arg//// This matches what we see in foo7's type signature:// lean_obj_res foo7(uint8_t mybool, lean_obj_arg world);// ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^// first arg second arg//// This also implies that the returned `lean_obj_res` will in fact be a `Result` object,// and indeed we will later see that we will use functions like `lean_io_result_mk_ok`// to construct `Result` Lean4 objects and return them back to our Lean4 program.
Here is another IO function, it returns aUInt32
(a special type), but in fact we return alean_obj_res
unlikefoo6
because IO functions return aResult
object.
// @[extern "foo8"]// opaque foo8 : (mybool : Bool) -> IO UInt32lean_obj_resfoo8(uint8_tmybool,lean_obj_argworld);// [!] IMPORTANT! Even if we are technically returning a UInt32, we still return a// `lean_obj_res` because we are suppose return a `Result` Lean4 object, of which// will contain our uint32_t result.
Now, hopefully you can see now whylean_glfwInit
has this type signature:
// @[extern "lean_glfwInit"]// opaque glfwInit : IO Boollean_obj_reslean_glfwInit(lean_obj_argworld);
DISCLAIMER: THE FOLLOWING DETAILS COULD BE VERY, VERY WRONG, but the final code does produce a working GLFW program.
Let's implementlean_glfwInit
:
lean_obj_reslean_glfwInit(lean_obj_argworld) {// `world` is unused.intresult=glfwInit();returnlean_io_result_mk_ok(lean_box(result));}
Here, we callglfwInit()
, and call some functions provided bylean/lean.h
to build alean_obj_res
as a Lean4Result
object as required byIO
functions, where...
// Construct a `lean_object *` representing `Result.ok r ()`lean_obj_reslean_io_result_mk_ok(lean_obj_argr);// Construct a `lean_object *` representing the signed integer nlean_object*lean_box(size_tn);
Hopefully this is enough to explain why we are writing this to build ourlean_obj_res
:
returnlean_io_result_mk_ok(lean_box(result));// ~ Result IO.Error IO.RealWorld Bool
You can now try usingopaque glfwInit : IO Bool
in./Main.lean
:
@[extern "lean_glfwInit"]opaque glfwInit : IO Bool-- ...main : IO Unitmain = do IO.println "Initializing GLFW" let init_ok <- glfwInit unless init_ok do IO.println "Cannot initialize GLFW!!" return // end program IO.println "Initialized GLFW!!"
Let's move on toglfwTerminate
andglfwPollEvents
, although they are not so interesting:
// @[extern "lean_glfwTerminate"]// opaque glfwTerminate : IO Unitlean_obj_reslean_glfwTerminate(lean_obj_argworld) {glfwTerminate();// [!] lean_box(0) means 0 or an Unit object.returnlean_io_result_mk_ok(lean_box(0)); }// @[extern "lean_glfwPollEvents"]// opaque glfwPollEvents : IO Unitlean_obj_reslean_glfwPollEvents(lean_obj_argworld) {// In fact, `world` is also just lean_box(0)... I think.glfwPollEvents();returnlean_io_result_mk_ok(lean_box(0));}
Let's get toglfwCreateWindow
. Here, things get more complicated because we have to handle the opaque typeGLFWwindow *
:
staticvoidnoop_foreach(void*data,b_lean_obj_argarg) {// NOTHING}staticvoidglfw_window_finalizer(void*ptr) {glfwDestroyWindow((GLFWwindow*)ptr);}// opaque WindowP : NonemptyType// def Window := WindowP.typestaticlean_external_class*get_glfw_window_class() {staticlean_external_class*g_glfw_window_class=NULL;if (!g_glfw_window_class) {g_glfw_window_class=lean_register_external_class(&glfw_window_finalizer,&noop_foreach); }returng_glfw_window_class;}// @[extern "lean_glfwCreateWindow"]// opaque glfwCreateWindow (width : UInt32) (height : UInt32) (title : String) : IO Windowlean_obj_reslean_glfwCreateWindow(uint32_twidth,uint32_theight,b_lean_obj_argtitle,lean_obj_argworld) {printf("lean_glfwCreateWindow %dx%d\n",width,height);// Here for debuggingcharconst*title_cstr=lean_string_cstr(title);// Read below for what `lean_string_cstr` doesGLFWwindow*win=glfwCreateWindow(width,height,title,NULL,NULL);// Returns (GLFWwindow *) -- an opaque type!returnlean_io_result_mk_ok(lean_alloc_external(get_glfw_window_class(),win));}// where...// Suppose o is a `String` Lean4 object, get the pointer pointing to the UTF8 bytes of the String.charconst*lean_string_cstr(b_lean_obj_argo);
Here, we see new functions related to constructinglean_object *
from custom pointers (e.g.,GLFWwindow *
):
lean_alloc_external
lean_external_class
lean_register_external_class
You already know whatlean_io_result_mk_ok
does.
Here is the function signature oflean_alloc_external
fromlean/lean.h
:
// Constructs a `lean_object *` representing an externally-defined C object// cls: Lean4's representation of the "type" of the externally-defined C object// data: the pointer to that externally-defined C objectlean_object*lean_alloc_external(lean_external_class*cls,void*data);
lean_external_class
is used by Lean4 internally to understand more about thevoid *data
you pass in. Itapparently has two purposes:
- to tell how to deallocate/finalize
data
when thelean_object *
goes out of scope in your Lean4 program, - (My guess) to tell how to iterate over
data
. (Like how you can havechar *c
and doc + 3
to get the address of the 3rdchar
by displacement.)
Here is the definition oflean_external_class
if that is of any help:
typedefvoid (*lean_external_finalize_proc)(void*data);typedefvoid (*lean_external_foreach_proc)(void*data,b_lean_obj_arg*idk_what_this_arg_is_for);typedefstruct {lean_external_finalize_procm_finalize;lean_external_foreach_procm_foreach;}lean_external_class;
It is okay if you don't understand whatlean_external_class
actually does, because I also don't.
Anyway, recall that to create alean_object *
out ofGLFWwindow *win
, we need to dolean_alloc_external(get_glfw_window_class(), win)
, which then will be wrapped bylean_lean_io_result_mk_ok
. Having already explained whatlean_alloc_external()
is, I will now explain theget_glfw_window_class()
part.
Here is the functionget_glfw_window_class
again:
// opaque WindowP : NonemptyType// def Window := WindowP.typestaticlean_external_class*get_glfw_window_class() {staticlean_external_class*g_glfw_window_class=NULL;if (!g_glfw_window_class) {g_glfw_window_class=lean_register_external_class(&glfw_window_finalizer,&noop_foreach); }returng_glfw_window_class;}// where...typedefvoid (*lean_external_finalize_proc)(void*);typedefvoid (*lean_external_foreach_proc)(void*,b_lean_obj_arg);// Creates a (lean_external_class *), for use in lean_alloc_external()lean_external_class*lean_register_external_class(lean_external_finalize_proc,lean_external_foreach_proc);
Inget_glfw_window_class
, essentially, we are callinglean_register_external_class
to tell Lean4 to create alean_external_class
representingGLFWwindow *
withglfw_window_finalizer
as its finalizer andnoop_foreach
as its "foreach" part,
whereglfw_window_finalizer
is defined as...
staticvoidglfw_window_finalizer(void*ptr) {// Here, I defined it to destroy the GLFWwindow object as an example.// In reality, I would put nothing here, and instead write a new opaque Lean4 function + C function for this destroying windows explicitly.glfwDestroyWindow((GLFWwindow*)ptr);}
...andnoop_foreach
is defined as:
staticvoidnoop_foreach(void*data,b_lean_obj_argarg) {// NOTHING}
I am usingnoop_foreach
for thelean_external_class
representingGLFWwindow *
here becauseGLFWwindow *
points to an handle object instead of the start of an array of something. There is no need for a "foreach" functionality.
get_glfw_window_class
is also written in such a way that the first call toget_glfw_window_class
will register thelean_external_class
that representsGLFWwindow *
and return it, and subsequent classes will return the already createdlean_external_class
. I am making use of a function-scopedstatic
variable here to do this trick.
Now, this should clarify whatlean_glfwCreateWindow
does.
As for the opaqueWindow
/WindowP
definition in our./Main.lean
, we as programmers will have to make sure that whenever aGLFWwindow *
is the return type or a argument type in our functions, we must useWindow
in our Lean4 code.
opaque WindowP : NonemptyTypedef Window := WindowP.type@[extern "lean_glfwCreateWindow"]opaque glfwCreateWindow (width : UInt32) (height : UInt32) (title : String) : IO Window // [!] Returns `Window`// lean_obj_res lean_glfwCreateWindow(uint32_t width, uint32_t height, b_lean_obj_arg title, lean_obj_arg world);@[extern "lean_glfwWindowShouldClose"]opaque glfwWindowShouldClose (win : Window) : IO Bool // [!] Takes in `Window`// lean_obj_res lean_glfwWindowShouldClose(lean_obj_arg winp, lean_obj_arg world);
Finally, let's implementlean_glfwWindowShouldClose
:
// @[extern "lean_glfwWindowShouldClose"]// opaque glfwWindowShouldClose (win : Window) : IO Bool // [!] Takes in `Window`lean_obj_reslean_glfwWindowShouldClose(lean_obj_argwinp,lean_obj_argworld) {assert(lean_is_external(winp));// For debugging// This is how we extract the `void *data` from a `lean_obj_arg` if the arg is indeed representing some external data.GLFWwindow*win= (GLFWwindow*)lean_get_external_data(winp);boolstatus=glfwWindowShouldClose(win);returnlean_io_result_mk_ok(lean_box(status));}// where...void*lean_get_external_data(lean_object*o);
Hopefully you can understand the definitionlean_glfwWindowShouldClose
without any explanations now.
And that is it!
We can now implement the following C code:
#include<GLFW/glfw3.h>#include<stdlib.h>intmain() {if (!glfwInit()) {perror("Cannot initialize GLFW!!\n");returnEXIT_FAILURE; }GLFWwindow*win=glfwCreateWindow(800,600,"My GLFW window's title",NULL,NULL);while (!glfwWindowShouldClose(win)) {glfwPollEvents(); }glfwTerminate();printf("Goodbye.\n");}
...in Lean4:
def main : IO Unit := do unless (<- glfwInit) do (<- IO.getStderr).putStrLn "Cannot initialize GLFW!!" return -- TODO: how to exit with EXIT_FAILURE? let win <- glfwCreateWindow 800 600 "My GLFW window's title" while (not (<- glfwWindowShouldClose win)) do glfwPollEvents glfwTerminate (<- IO.getStdout).putStrLn "Goodbye."
All the code is in this repository. Do$ ./run.sh
to build and run the final GLFW example app.
- Investigate on constructing
lean_obj_res *
objects of Lean4 inductive types in C. - Understand what borrowing is and how it affects programming an FFI.
About
Lean4 Tutorial/Notes on creating FFI bindings with GLFW as an example.
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.