BNFC 生成的 C 解析器中的内存管理

问题描述

我使用 bnfc 生成解析器 bnfc -m -c ./mylang.cf。在内部,bnfc makefile 调用 bison 生成 C 解析器。

Parser.c : mylang.y
    ${BISON} ${BISON_OPTS} mylang.y -o Parser.c

我可以通过调用下面生成psProc 方法成功解析源代码

/* Global variables holding parse results for entrypoints. */
Proc YY_RESULT_Proc_ = 0;
// ...

/* Entrypoint: parse Proc from string. */
Proc psProc(const char *str)
{
  YY_BUFFER_STATE buf;
  mylang__init_lexer(0);
  buf = mylang__scan_string(str);
  int result = yyparse();
  mylang__delete_buffer(buf);
  if (result)
  { /* Failure */
    return 0;
  }
  else
  { /* Success */
    return YY_RESULT_Proc_;
  }
}

struct Proc_;
typedef struct Proc_ *Proc;

struct Proc_
{
  enum { is_PGround,is_PCollect,is_PVar,is_PVarRef,is_PNil,is_PSimpleType,is_PNegation,is_PConjunction,is_Pdisjunction,is_PEval,is_PMethod,is_PExprs,is_PNot,is_PNeg,is_PMult,is_PDiv,is_PMod,is_PPercentPercent,is_PAdd,is_PMinus,is_PPlusPlus,is_PMinusMinus,is_plt,is_plte,is_PGt,is_PGte,is_PMatches,is_PEq,is_PNeq,is_PAnd,is_POr,is_PSend,is_PContr,is_PInput,is_PChoice,is_PMatch,is_PBundle,is_PIf,is_PIfElse,is_PNew,is_PPar } kind;
  union
  {
    struct { Ground ground_; } pground_;
    struct { Collection collection_; } pcollect_;
    struct { ProcVar procvar_; } pvar_;
    struct { Var var_; VarRefKind varrefkind_; } pvarref_;
    struct { SimpleType simpletype_; } psimpletype_;
    struct { Proc proc_; } pnegation_;
    struct { Proc proc_1,proc_2; } pconjunction_;
    struct { Proc proc_1,proc_2; } pdisjunction_;
    struct { Name name_; } peval_;
    struct { ListProc listproc_; Proc proc_; Var var_; } pmethod_;
    struct { Proc proc_; } pexprs_;
    struct { Proc proc_; } pnot_;
    struct { Proc proc_; } pneg_;
    struct { Proc proc_1,proc_2; } pmult_;
    struct { Proc proc_1,proc_2; } pdiv_;
    struct { Proc proc_1,proc_2; } pmod_;
    struct { Proc proc_1,proc_2; } ppercentpercent_;
    struct { Proc proc_1,proc_2; } padd_;
    struct { Proc proc_1,proc_2; } pminus_;
    struct { Proc proc_1,proc_2; } pplusplus_;
    struct { Proc proc_1,proc_2; } pminusminus_;
    struct { Proc proc_1,proc_2; } plt_;
    struct { Proc proc_1,proc_2; } plte_;
    struct { Proc proc_1,proc_2; } pgt_;
    struct { Proc proc_1,proc_2; } pgte_;
    struct { Proc proc_1,proc_2; } pmatches_;
    struct { Proc proc_1,proc_2; } peq_;
    struct { Proc proc_1,proc_2; } pneq_;
    struct { Proc proc_1,proc_2; } pand_;
    struct { Proc proc_1,proc_2; } por_;
    struct { ListProc listproc_; Name name_; Send send_; } psend_;
    struct { ListName listname_; Name name_; NameRemainder nameremainder_; Proc proc_; } pcontr_;
    struct { Proc proc_; Receipt receipt_; } pinput_;
    struct { ListBranch listbranch_; } pchoice_;
    struct { ListCase listcase_; Proc proc_; } pmatch_;
    struct { Bundle bundle_; Proc proc_; } pbundle_;
    struct { Proc proc_1,proc_2; } pif_;
    struct { Proc proc_1,proc_2,proc_3; } pifelse_;
    struct { ListNameDecl listnamedecl_; Proc proc_; } pnew_;
    struct { Proc proc_1,proc_2; } ppar_;
  } u;
};

我有几个关于 Proc psProc(const char *str) 的问题。

  1. 我可以在 char *str 返回后立即释放 psProc 参数引用的源缓冲区吗?我猜返回的 Proc 可能包含指向输入源缓冲区的指针,所以我应该确保源缓冲区的生命周期长于返回的指针。对吗?

  2. 我应该如何释放返回的 Proc?返回的Proc一个指向Proc_的指针,它由指针组成一个抽象语法树。我只需要在返回的指针上调用一次 free() 即可释放,对吗?

  3. Proc psProc(const char *str)方法体中,它返回一个存储在全局变量YY_RESULT_Proc_中的指针。这是否意味着我不能从不同的线程同时调用 psProc

解决方法

此类问题应在该工具的文档中得到解答。但是我在那里找不到它们:-(我对 BNFC 也没有很多经验,所以应用这个答案要谨慎。

  1. psProc 调用词法分析器的 scan_string 接口,该接口制作提供的字符串的副本。 Flex 喜欢在标记化时修改输入,因此它只能通过复制来处理 const char* 输入。因此,字符串可以在调用 scan_string 后立即被释放,但 psProc 在返回之前解析整个输入,因此您不必这样做。您当然可以在 psProc 返回时释放字符串。

    我怀疑这对您来说是否是个问题,但如果您打算解析非常大的内存字符串,您可能需要考虑使用 fmemopen(至少,在 Posix 平台上)打开字符串作为 FILE*。这并不能避免复制,而是以 8k 左右的块为单位进行复制,从而避免在解析过程中保留整个字符串的两个副本。

  2. 我不知道 BNFC 如何期望您释放解析树节点。 (事实上​​,我宁愿怀疑它不希望你这样做。)节点与内部指针链接,当然可以编写一个 AST walker,它使用 post- 递归地释放所有节点顺序遍历。但我没有看到任何生成的代码可以做到这一点。可能是我看的不够仔细。

    在顶级节点上调用 free() 只会释放一个节点。树的其余部分将被泄漏,因为不存在其他指向它的指针。

  3. 我很确定你对线程安全的怀疑是正确的。全局由 Proc 产生式的归约操作分配,然后由 psProc 返回。没有锁定,所以如果另一个线程中有另一个解析器,全局可能会被覆盖。全局只是一个指向要返回的节点的指针,节点本身应该是线程安全的,因为它是由解析器线程动态分配的。因此,您可能会更改 global(s) 的声明以使用线程本地存储,但这必须通过对生成的代码进行后处理来完成。