如何在C++/Ocaml之间安全地转换树数据结构?

How to safely translate tree data structures between C++ / Ocaml?

本文关键字:转换 数据结构 安全 之间 C++ Ocaml      更新时间:2023-10-16

我有一个用C++编写的遗留数据结构和OCaml中的新工具,预计将处理遗留数据。所以我需要将数据从前者导入/转换为后者。数据采用树的形式,通常由访问者处理。

作为一个简单的示例,请考虑以下最小DSL:

#include <memory>
using namespace std;
class intnode;
class addnode;
struct visitor {
virtual void visit(const intnode& n) = 0;
virtual void visit(const addnode& n) = 0;
};
struct node {
virtual void accept(visitor& v) = 0;
};
struct intnode : public node {
int x;
virtual void accept(visitor& v) { v.visit(*this); }
};
struct addnode : public node {
shared_ptr<node> l;
shared_ptr<node> r;
virtual void accept(visitor& v) { v.visit(*this); }
};

它在 OCaml 中的表示形式如下所示:

type node = Int of int
| Plus of node * node
let make_int x = Int x
let make_plus l r = Plus(l,r)

问题是,如何安全有效地将C++树转换为其 OCaml 表示形式?

到目前为止,我有两种方法:

方法1

编写一个调用 OCaml 构造函数并生成value的访问者,例如:

value translate(shared_ptr<node> n);
struct translator : public visitor {
value retval;
virtual visit(const intnode& n) {
retval = call(make_int, Val_int(x->value));
}
virtual visit(const addnode& n) {
value l = translate(n.l);
value r = translate(n.r);
retval =  call(make_add, l, r);
}
};
value translate(shared_ptr<node> n)
{
translator t;
t.visit(*n);
}

简单地假设call执行所有必需的基架来回调 OCaml 并调用正确的构造函数。

这种方法的问题在于OCaml的garbag收集器。如果 GC 运行,而C++端在 is 堆栈上有一些value,则该值(毕竟是进入 OCaml 堆的指针)可能会失效。所以我需要一些方法来通知 OCaml 仍然需要这些值的事实。通常这是使用 CAML* 宏完成的,但在这样的情况下我该怎么做呢?我可以在visit方法中使用这些宏吗?

方法2

第二种方法更为复杂。当无法安全地存储中间引用时,我可以扭转局面并将C++指针推送到 OCaml 堆中:

type cppnode (* C++ pointer *)
type functions = {
transl_plus : cppnode -> cppnode -> node;
transl_int : int -> node;
}
external dispatch : functions -> cppnode -> node = "dispatch_transl"
let rec translate n = dispatch {transl_plus; transl_int = make_int} n
and transl_plus a b = make_plus (translate a) (translate b)

这里的想法是,函数"dispatch"将所有子节点包装到CustomVal结构中,并将它们传递给OCaml,而不存储任何中间值。相应的访问者将仅实现模式匹配。这显然应该适用于 GC,但缺点是效率略低(由于指针换行)和可能不太可读(因为调度和重建之间的区别)。

有没有办法用方法1的优雅来获得方法2的安全性?

即使在递归的情况下,我也看不到在 C 堆栈上构造 OCaml 值有任何问题。在您的示例中,您使用结构成员来存储 OCaml 堆值。这也是可能的,但是,您需要使用caml_register_global_rootcaml_register_generational_rootcaml_remove_global_rootcaml_remove_generational_global_root释放它们。事实上,您甚至可以构建一个智能指针来保存 OCaml 值。

综上所述,我仍然看不出任何理由(至少对于您演示的简化示例)为什么要为此进入类成员,这就是我解决它的方法:

struct translator : public visitor {
virtual value visit(const intnode& n) {
CAMLparam0();
CAMLlocal1(x);
x = call(make_int, Val_int(n->value);
CAMLreturn(x);
}
virtual value visit(const addnode& n) {
CAMLparam0();
CAMLlocal(l,r,x);
l = visit(*n.l);
r = visit(*n.r);
x = call(make_add, l, r);
CAMLreturn(x);
}
};

当然,这是假设您有一个可以返回任意类型值的访问者。如果你没有,并且不想实现一个,那么你绝对可以逐步建立你的价值:

value translate(shared_ptr<node> n);
class builder : public visitor {
value result;
public:
builder() {
result = Val_unit; // or any better default
caml_register_generational_global_root(&result);
}
virtual ~builder() {
caml_remove_generational_global_root(&result);
}
virtual void visit(const intnode& n) {
CAMLparam0();
CAMLlocal1(x);
x = call(make_int, Val_int(n->value);
caml_modify_generational_global_root(&result, x);
CAMLreturn0;
}
virtual void visit(const addnode& n) {
CAMLparam0();
CAMLlocal(l,r,x);
l = translate(n.l);
r = translate(n.r);
x = call(make_add, l, r);
caml_modify_generational_global_root(&result,x)
CAMLreturn0;
}
};
value translate(share_ptr<node> node) {
CAMLparam0();
CAMLlocal1(x);
builder b;
b.visit(*node);
x = b.result;
CAMLreturn(x);
}

你也可以看看Berke Durak的Aurochs项目,该项目使用C构建解析树。

就个人而言,我会用 C++ 编写一个转储程序,并在 OCaml 中编写该转储的解析器。 如果你不被更复杂的路线吓到,也许你可以看看这个工具: https://github.com/Antique-team/clangml