I am trying to build a C++ library backed by Coq sets, but I am continuously running into segfaults which I believe are related to garbage collection or memory leaks. I am using Coq 8.17.1 with a fresh build of CertiCoq (https://github.com/CertiCoq/certicoq) as of May 14 2024. In this setting, I am using CertiCoq to compile a Coq structure that consists of the usual set operations---in this case, empty, membership, add, and cardinal. (Full MWE at the bottom of the page).
Record MSet_struct A S :=
MSet_Struct {
mset_empty : S;
mset_mem : A -> S -> bool;
mset_add : A -> S -> S;
mset_remove : A -> S -> S;
mset_cardinal : S -> int;
}.
…
Module RBT := Coq.MSets.MSetRBT.Make int_as_OT.
Definition RBT_MSet_struct : MSet_struct int RBT.t :=
{|
mset_empty := RBT.empty;
mset_mem := RBT.mem;
mset_add := RBT.add;
mset_remove := RBT.remove;
mset_cardinal := fun x => nat_to_int (RBT.cardinal x);
|}.
CertiCoq Generate Glue -file "glue" [bool].
CertiCoq Compile -file "RBT" RBT_MSet_struct
Extract Constants []
Include ["stdbool.h" as library].
Then, in a .cpp file I define a class called certicoq::set
, which is a set of integers. The class has an underlying value
which stores the set and implements the usual methods.
class set {
private:
// the value underlying the set
value t_value_;
public:
value getValue() const { return t_value_; };
void setValue(value v) { t_value_ = v; };
// Constructors and destructors
set(value);
set(); // empty set
~set() = default;
void add(int x);
void remove(int x); // if x is not already in the set, do nothing
bool isMember(int x) const;
int size() const;
};
The implementation of each library function (e.g. add, cardinal, mem) is defined via one or more call
s to the Coq runtime. For example:
void set::add(int x) {
value vs = getValue();
value vx = int_to_value(x);
value f = get_args(body(tinfo_))[set_add_tag];
value f0 = LIVEPOINTERS3(tinfo_, call(tinfo_, f, vx), f, vx, vs);
value v = LIVEPOINTERS2(tinfo_, call(tinfo_, f0, vs), f0, vs);
setValue(v);
}
Here, LIVEPOINTERS are macros meant to tell the Coq garbage collector the live pointers a la [1] and [2]. Note that all set values are constructed using API calls, meaning that they all should live on the Coq heap. The only values built outside the Coq heap are primitives such as booleans, primitive integers, and 0-ary constructors.
We can use this library as shown in this main
function in CoqSet.cpp:
int main() {
certicoq::initialize_global_thread_info();
certicoq::set X;
for (int i=0; i<1000; i++) {
std::cout << "Adding " << i << "; ";
X.add(i);
std::cout << "new set has size: " << X.size() << "\n";
}
}
However, after adding only 133 integers, the size of the set resets to zero, and after another 133 integers, I get a segmentation fault. I can get this slightly better by changing how LIVEPOINTERS are used or explicitly making calls to the garbage collector, but even then it is still quite limited. I suspect I am not interacting with the garbage collector properly, but I am not sure what the solution should look like.
A MWE example is shown below.
MSetImplementation.v
Require Import MSets.MSetRBT.
From CertiCoq.Plugin Require Import CertiCoq.
Require Import Coq.Structures.Orders.
From Coq.Numbers.Cyclic.Int63 Require Import PrimInt63.
Require Import CertiCoq.Plugin.PrimInt63.
(* API for MSets, which can be instantiated with any MSet implementation. *)
Record MSet_struct A S :=
MSet_Struct {
mset_empty : S;
mset_mem : A -> S -> bool;
mset_add : A -> S -> S;
mset_cardinal : S -> int;
}.
(* Skeleton definition of primitive ints as an ordered type *)
Module int_as_OT <: UsualOrderedType.
Definition t := int.
Definition eq := @eq int.
Definition eq_refl := @eq_refl t.
Definition eq_sym := @eq_sym t.
Definition eq_trans := @eq_trans t.
Definition eq_equiv : Equivalence eq.
Proof.
constructor. exact eq_refl. exact eq_sym. exact eq_trans.
Defined.
Definition lt := fun z1 z2 => compare z1 z2 = Lt.
Lemma lt_trans : forall x y z : t, lt x y -> lt y z -> lt x z.
Admitted.
Lemma lt_not_eq : forall x y : t, lt x y -> ~ eq x y.
Admitted.
Lemma int_eqb_eq : forall (x y : int),
eqb x y = true <-> x = y.
Admitted.
Lemma leb_implies_gtb : forall (x y : int),
ltb x y = false ->
eqb x y = false ->
ltb y x = true.
Admitted.
Lemma lt_strorder : StrictOrder lt.
Admitted.
Lemma lt_compat : Proper (Logic.eq ==> Logic.eq ==> iff) lt.
Admitted.
Definition compare : int -> int -> comparison := compare.
Lemma compare_spec : forall x y : t,
CompareSpec (x = y) (lt x y) (lt y x) (compare x y).
Admitted.
Lemma compare_eq : forall x y, compare x y = Eq -> x = y.
Admitted.
Lemma compare_lt : forall x y, compare x y = Lt -> lt x y.
Admitted.
Lemma compare_gt : forall x y, compare x y = Gt <-> compare y x = Lt.
Admitted.
Definition eq_dec : forall x y : t, { eq x y } + { ~ eq x y }.
Proof.
intros x y.
destruct (compare x y) eqn:Hxy.
* left. apply compare_eq. exact Hxy.
* right. apply lt_not_eq. exact Hxy.
* right. apply compare_gt in Hxy. apply lt_not_eq in Hxy.
intros Heq. apply Hxy. symmetry. exact Heq.
Defined.
End int_as_OT.
Fixpoint nat_to_int (x : nat) : int :=
match x with
| 0 => 0
| S x' => add 1 (nat_to_int x')
end.
(* Instantiate red black trees (RBT) MSets with integers. *)
Module RBT := Coq.MSets.MSetRBT.Make int_as_OT.
Definition RBT_MSet_struct : MSet_struct int RBT.t :=
{|
mset_empty := RBT.empty;
mset_mem := RBT.mem;
mset_add := RBT.add;
mset_cardinal := fun x => nat_to_int (RBT.cardinal x);
|}.
CertiCoq Generate Glue -file "glue" [bool].
CertiCoq Compile -file "RBT" RBT_MSet_struct
Extract Constants []
Include ["stdbool.h" as library].
CoqSet.cpp
#include <iostream>
extern "C" {
// Files found in path-to-certicoq/plugin/runtime/
#include "values.h"
#include "gc_stack.h"
#include "prim_int63.h"
extern struct thread_info *make_tinfo();
extern value *get_args(value);
extern value call(struct thread_info *, value, value);
value body(struct thread_info *);
extern value make_Coq_Init_Datatypes_bool_true(void);
extern value make_Coq_Init_Datatypes_bool_false(void);
extern unsigned int get_Coq_Init_Datatypes_bool_tag(value);
extern void print_Coq_Init_Datatypes_bool(value);
}
namespace certicoq {
// Global thread info
static struct thread_info* tinfo_ = NULL;
void initialize_global_thread_info() {
if (tinfo_ == NULL) {
tinfo_ = make_tinfo();
}
}
// Set of integers data structure
class set {
private:
// the value underlying the set
value t_value_;
public:
value getValue() const { return t_value_; };
void setValue(value v) { t_value_ = v; };
// Constructors and destructors
set(); // empty set
~set() = default;
void add(int x);
bool isMember(int x) const;
int size() const;
};
///////////////////
// Instantiation //
///////////////////
// This enum copies the order of constructors of MSet_struct from
// MSetImplementation.v
enum SetOpsTag {
set_empty_tag,
set_mem_tag,
set_add_tag,
set_cardinal_tag
};
//////////////////////
// Helper functions //
//////////////////////
bool value_to_bool(const value b) {
return (get_Coq_Init_Datatypes_bool_tag(b) == 0);
}
value bool_to_value(const bool b) {
if (b) {
return make_Coq_Init_Datatypes_bool_true();
} else {
return make_Coq_Init_Datatypes_bool_false();
}
}
int value_to_int(const value v) {
return Int_val((long) v);
}
value int_to_value(const int x) {
return (value)Val_int(x);
}
// These macros are taken from VeriFFI examples,
// e.g. https://github.com/CertiCoq/VeriFFI/blob/main/examples/array/prims.c
// See https://github.com/CertiCoq/certicoq/wiki/When-C-code-allocates-on-the-Coq-heap
// for more info
#define LIVEPOINTERS1(tinfo, exp, a) ({ \
value __ROOT__[1] = { a }; \
struct stack_frame __FRAME__ = { __ROOT__ + 1, __ROOT__, tinfo->fp }; \
tinfo->fp = &__FRAME__; value __TEMP__ = exp; \
a = __ROOT__[0]; \
tinfo->fp = __FRAME__.prev; __TEMP__; })
#define LIVEPOINTERS1_(tinfo, exp, a) ({ \
value __ROOT__[1] = { a }; \
struct stack_frame __FRAME__ = { __ROOT__ + 1, __ROOT__, tinfo->fp }; \
tinfo->fp = &__FRAME__; \
a = __ROOT__[0]; \
tinfo->fp = __FRAME__.prev; })
#define LIVEPOINTERS2(tinfo, exp, a, b) ({ \
value __ROOT__[2] = { a, b }; \
struct stack_frame __FRAME__ = { __ROOT__ + 2, __ROOT__, tinfo->fp }; \
tinfo->fp = &__FRAME__; value __TEMP__ = exp; \
a = __ROOT__[0]; b = __ROOT__[1]; \
tinfo->fp = __FRAME__.prev; __TEMP__; })
#define LIVEPOINTERS2_(tinfo, exp, a, b) ({ \
value __ROOT__[2] = { a, b }; \
struct stack_frame __FRAME__ = { __ROOT__ + 2, __ROOT__, tinfo->fp }; \
tinfo->fp = &__FRAME__; exp; \
a = __ROOT__[0]; b = __ROOT__[1]; \
tinfo->fp = __FRAME__.prev; })
#define LIVEPOINTERS3(tinfo, exp, a, b, c) ({ \
value __ROOT__[3] = { a, b, c }; \
struct stack_frame __FRAME__ = { __ROOT__ + 3, __ROOT__, tinfo->fp }; \
tinfo->fp = &__FRAME__; value __TEMP__ = exp; \
a = __ROOT__[0]; b = __ROOT__[1]; c = __ROOT__[2]; \
tinfo->fp = __FRAME__.prev; __TEMP__; })
//////////////////
// Constructors //
//////////////////
// Empty set
set::set() {
value v = get_args(body(tinfo_))[set_empty_tag];
setValue(v);
}
/////////////////////
// set Operations //
/////////////////////
void set::add(int x) {
value vs = getValue();
value vx = int_to_value(x);
value f = get_args(body(tinfo_))[set_add_tag];
value f0 = LIVEPOINTERS3(tinfo_, call(tinfo_, f, vx), f, vx, vs);
value v = LIVEPOINTERS2(tinfo_, call(tinfo_, f0, vs), f0, vs);
setValue(v);
}
bool set::isMember(int x) const {
value vX = getValue();
value vx = int_to_value(x);
value f = get_args(body(tinfo_))[set_mem_tag];
value f0 = LIVEPOINTERS3(tinfo_, call(tinfo_, f, vx), f, vx, vX);
value v = LIVEPOINTERS2(tinfo_, call(tinfo_, f0, vX), f0, vX);
return value_to_bool(v);
}
int set::size() const {
value vX = getValue();
value f = get_args(body(tinfo_))[set_cardinal_tag];
value v = LIVEPOINTERS2(tinfo_, call(tinfo_, f, vX), f, vX);
return value_to_int(v);
}
}
int main() {
certicoq::initialize_global_thread_info();
certicoq::set X;
for (int i=0; i<1000; i++) {
std::cout << "Adding " << i << "; ";
X.add(i);
std::cout << "new set has size: " << X.size() << "\n";
}
}
Makefile
PATH_TO_CERTICOQ="../../certicoq"
all:
coqc MSetImplementation.v
make -C ../../certicoq/plugin/runtime all
clang -o RBT.o -Wno-everything -O2 -fomit-frame-pointer -I $(PATH_TO_CERTICOQ)/plugin/runtime -c RBT.c
clang -o glue.o -Wno-everything -O2 -fomit-frame-pointer -I $(PATH_TO_CERTICOQ)/plugin/runtime -c glue.c
clang++ -o CoqSet.o -O2 -fomit-frame-pointer -I $(PATH_TO_CERTICOQ)/plugin/runtime -c CoqSet.cpp
clang++ -o CoqSet $(PATH_TO_CERTICOQ)/plugin/runtime/gc_stack.o $(PATH_TO_CERTICOQ)/plugin/runtime/prim_int63.o \
RBT.o glue.o CoqSet.o -lm
[1] https://github.com/CertiCoq/VeriFFI/blob/main/examples/array/prims.c
[2] https://github.com/CertiCoq/certicoq/wiki/When-C-code-allocates-on-the-Coq-heap
============================ Edit:
Taking Joomy's advice, I motified the member functions as follows:
void set::add(int x) {
BEGINFRAME(tinfo_, 4)
value vs = getValue();
value vx = int_to_value(x);
nalloc=10000; GC_SAVE2(tinfo_, vs, vx)
value f = get_args(body(tinfo_))[set_add_tag];
value f0 = LIVEPOINTERS3(tinfo_, call(tinfo_, f, vx), f, vx, vs);
value v = LIVEPOINTERS2(tinfo_, call(tinfo_, f0, vs), f0, vs);
setValue(v);
ENDFRAME
}
where
#define GC_SAVE2(tinfo, save0, save1) \
if (!(_LIMIT=tinfo->limit, _ALLOC=tinfo->alloc, nalloc <= _LIMIT-_ALLOC)) { \
tinfo->nalloc = nalloc; \
LIVEPOINTERS2(tinfo,(garbage_collect(tinfo),(value)NULL),save0,save1); \
}
It is not clear to me how the value of nalloc
should be calculated, and while high constant values as shown here improve performance (allow me to build a larger set), it does not work forever.
add
, if I havenalloc=100; GC_SAVE2(tinfo_, vs, vx)
there is no improvement, but if I havenalloc=10000; GC_SAVE2(tinfo_, vs, vx)
, then I am able to run the main function without errors. $\endgroup$