这篇文章同时发布在github上

这篇文章是我对ooc编译器里一个小bug调试时作的手记。虽然相信大多数人对编译器(并且是一门小众语言的编译器)并不感兴趣,但这篇文章可以给C用户们提供一些Object-oriented Programming的想法,以及是对之前那篇泛型文章的最好的补充。我自己都没想到在翻译了那篇文章没多久,就亲身经历了这么一个“光滑平面”的问题。

Introduction

今天,我在ooc-kean上看到了一个注释:

minimum: static func ~multiple(value: This, values: ...) -> This {
// FIXME: This creates a closure that causes a leak every time this function is called.
values each(|v|
if ((v as This) < value)
value = v
)
value
}

minimum是Float类型的一个扩展,这里用了闭包来实现对每个值的比较。如果这些发生在GC之下,那么一切都没有问题,因为任何内存(包括闭包的)都会被GC默默的回收。但OOC-Kean并没有打开GC,这里就出现了问题——闭包需要保存它的临时环境,于是每个闭包都会创建一个结构体,但编译器并没有考虑去回收它。因此就有了这里这个FIXME。

好吧,或许你还不是很明白为什么,那么让我们来看看这段代码最终生成的C代码:

lang_Numbers__Float lang_Numbers__Float_minimum_multiple(lang_Numbers__Float value, lang_VarArgs__VarArgs values) {
__FloatExtension_FloatExtension_closure4_ctx* __FloatExtension_ctx5 = lang_Memory__gc_malloc(((lang_types__Class*)__FloatExtension_FloatExtension_closure4_ctx_class())->size);
(*(__FloatExtension_ctx5)) = (__FloatExtension_FloatExtension_closure4_ctx) {
&(value)
};
lang_types__Closure __FloatExtension_closure6 = (lang_types__Closure) {
FloatExtension____FloatExtension_FloatExtension_closure4_thunk,
__FloatExtension_ctx5
};
lang_VarArgs__VarArgs_each(values, __FloatExtension_closure6);
return value;
}

让我们来慢慢解释这个函数。

  • 首先,对于没一个闭包(|x|..)编译器会首先生成一个闭包环境用来保存运行时的内容,这里,这个环境叫做__FloatExtension_ctx5,这是一个简单的结构体,随后,我们知道,每个closure都只能在当前Scope里有效,因此在每次使用之前,我们要为它分配新的内存,然后初始化。为什么要这么做?首先,我们可能打算模拟闭包那种随用随舍弃的特性,但这不是最大的原因。最大的问题是大部分Closure都是作为__First-class Function__使用的,如果之是定义之后简单调用的话,本来一切会很美好(我们可以简单的生成macro,或者在module里生成新的函数),但当他是一个指针时,在C语言的层面上,我们只能寻找一个替代品——这个替代品要能够使用当前scope已经存在的变量,函数,同时还能将一切反馈回来。这就是一个十分困难的问题。
  • 在这里,编译器在closure的背后做了许多事情,这里初始化的ctx5包含了一个庞大的初始化函数(未在源代码里显示),这个初始化会根据Scope里每个函数的类型和状态来决定是用Pointer(by reference)还是value。随后我们创建了一个普通的C函数并取它的指针,但它并不直接跟我们声明的closure有一样的参数,而是接受一个叫做__context__的参数,这个参数包含了所有之前的创建几个内容。
  • 最后,不管是我们直接调用闭包,还是把它当作指针使用,都可以通过统一的初始化函数生成ctxcontext了。

显然, 问题也就出在这里——为了使用context,我们分配的内存,但并没有考虑释放它。

The First Encount

第一个有的想法很单纯——既然没有释放,那么我们在Scope的最后加上一个gc_free不就行了么?那么让我们来试一试:

在生成closure之后,让我们添加一个Free的调用:

ctxFreeCall := FunctionCall new("gc_free", token)
ctxFreeCall args add(VariableAccess new(ctxDecl, token))
trail addAfterInScope(this, ctxFreeCall)

trail addAfterInScope会在当前Scope里当前Statement的后方生成对应的函数——当然,前提是编译器的栈没有问题。

然后,理所当然的,刚才的内存泄漏消失了:

lang_Numbers__Float lang_Numbers__Float_minimum_multiple(lang_Numbers__Float value, lang_VarArgs__VarArgs values) {
__FloatExtension_FloatExtension_closure4_ctx* __FloatExtension_ctx5 = lang_Memory__gc_malloc(((lang_types__Class*)__FloatExtension_FloatExtension_closure4_ctx_class())->size);
(*(__FloatExtension_ctx5)) = (__FloatExtension_FloatExtension_closure4_ctx) {
&(value)
};
lang_types__Closure __FloatExtension_closure6 = (lang_types__Closure) {
FloatExtension____FloatExtension_FloatExtension_closure4_thunk,
__FloatExtension_ctx5
};
lang_VarArgs__VarArgs_each(values, __FloatExtension_closure6);
lang_Memory__gc_free(__FloatExtension_ctx5);
return value;
}

不过,先别高兴,因为很快就有了新的问题——标准库的一些函数运行时抛出了segmentation fault。一个典型的例子是lang/format下面的一个函数,这个函数在字符串处理里有着关键的角色:

getEntityInfo: inline func (info: FSInfoStruct@, va: VarArgsIterator*, start: Char*, end: Pointer) {

	/* save original pointer */
p := start checkedInc := func {
if (p < end) p += 1
else InvalidFormatException new(start) throw()
} /* Find any flags. */
info flags = 0 while(p as Pointer < end) {
checkedInc()
match(p@) {
case '#' => info flags |= TF_ALTERNATE
case '0' => info flags |= TF_ZEROPAD
case '-' => info flags |= TF_LEFT
case ' ' => info flags |= TF_SPACE
case '+' => info flags |= TF_EXP_SIGN
case => break
}
} /* Find the field width. */
info fieldwidth = 0
while(p@ digit?()) {
if(info fieldwidth > 0)
info fieldwidth *= 10
info fieldwidth += (p@ as Int - 0x30)
checkedInc()
} /* Find the precision. */
info precision = -1
if(p@ == '.') {
checkedInc()
info precision = 0
if(p@ == '*') {
T := va@ getNextType()
info precision = argNext(va, T) as Int
checkedInc()
}
while(p@ digit?()) {
if (info precision > 0)
info precision *= 10
info precision += (p@ as Int - 0x30)
checkedInc()
}
} /* Find the length modifier. */
info length = 0
while (p@ == 'l' || p@ == 'h' || p@ == 'L') {
info length += 1
checkedInc()
} info bytesProcessed = p as SizeT - start as SizeT
}

看起来有些长,不过没关系,你不需要理解这个函数,因为引起segmentatian fault的其实只有几行,让我们把它单独拿出来:

	checkedInc := func {
if (p < end) p += 1
else InvalidFormatException new(start) throw()
}

这是什么? 对,闭包,那么我们可以想象到编译器做了什么,让我们来看看C代码:

__lang_Format_lang_Format_closure26_ctx* __lang_Format_ctx27 = lang_Memory__gc_malloc(((lang_types__Class*)__lang_Format_lang_Format_closure26_ctx_class())->size);
(*(__lang_Format_ctx27)) = (__lang_Format_lang_Format_closure26_ctx) {
end,
start,
&(p)
};
lang_types__Closure __lang_Format_closure28 = (lang_types__Closure) {
lang_Format____lang_Format_lang_Format_closure26_thunk,
__lang_Format_ctx27
};
lang_types__Closure checkedInc = __lang_Format_closure28;
gc_free(__lang_Format_closure28);
(*info).flags = 0;
while (((lang_types__Pointer) (p)) < end) {

没错,我们为checkedInc生成了一个很漂亮的运行环境,但问题来了,我们在当前Scope里,并且是当前Statement之后就释放了它,那么显然,如果任何后面的代码使用了这个闭包,那么我们得到的就是一个空指针。

解决方法似乎并不是很难,让我们仔细想想——我们到底什么时候才不要这个闭包,显然,如果这个闭包定义来自函数,那么就是这个函数返回时。让我们来实现它:

i := getSize() - 1
while(i>=0){
if(trail get(i) instanceOf?(FunctionDecl)){
fun := trail get(i) as FunctionDecl
for(i in fun body indexOf(this)..fun body size){
if(fun body[i] instanceOf?(Return)){
fun add(i, ctxFreeCall)
}
}
}
i -= 1
}

我们在每个return里都追加了free——并且它们一定要在当前statement之后。不过相信你一定已经注意到了潜在的问题——Function的Body不见得都是Statement,它有可能是If,有可能是Scope,还有可能是别的闭包,这个问题突然就变得麻烦起来了。

不过我们当然有办法,我们针对每个类型判断,然后决定是不是该递归去寻找Return:

if(fun body[i] instanceOf?(ControlStatement)){
for(i in fun body[i] as ControlStatement body){
// recursive do
}
} else if(fun body[i] instanceOf?(Scope)){
// ....
} else if ....

虽然很麻烦,但不是特别困难,如果问题到此为止,那么它远远算不上是complex,让我们再来想想,闭包还能怎么用? 对,初始化一个函数指针。如果这个发生在Function里面,那么一切都很好,但实际情况是——我们可以在Class里初始化一个函数指针,也可以在Cover里这么做。现在问题一下复杂起来了,因为定义在class里面的闭包并不属于一个函数,我们除了在Destroy(或者finalization)里面添加释放函数之外别无它法。但更大的问题是——我们无法保证class已经被完全解析!也就是说,在某些情况下,我们不知道这个class的任何信息,当然更不要说去添加释放函数。

然后,测试结果给了我们更大的打击——单纯的在Statement前面添加释放函数有时会破坏函数的结构!当我用上面的补丁对编译器进行boostrap的时候,它毫不客气的扔了一个错误,并且压根没有Backtrace。

为什么? 我想你已经注意到了,因为闭包可以作为指针被返回!

The Last, By Last

不知道你怎么看这个问题,至少上面这些东西就已经消耗了我整整一天的时间。并且问题还远远没有解决。

现在是时候来总结下原因了,简单来说,闭包可以作为指针被传来传去。在有GC时,GC会追踪它并且释放它,但当GC被关闭时,用户没法获得闭包真正的内容,因此甚至无法手动释放。而当闭包作为指针被传来传去时,我们找不到合适的时机来释放它。

那么我们来做最后一次尝试:我们追踪这个变量,一旦这个变量不再被使用时,我们就立刻追加一个gc_free,当然,这仅限于当前Scope,当它出了这个scope,我们就要求外界必须获得一个拷贝。这个实现也不难,就像这样:

addByLastUse: func(ctx: VariableDecl, marker: Statement, newcomer: Statement) -> Bool{
i := getSize() - 1
while(i >= 0) {
node := data get(i) as Node
if(node instanceOf?(Scope)){
sc := node as Scope
last := 0
j := sc list size - 1
while(j >= sc list indexOf(marker)){
if(accessed(sc list[j], ctx)){
sc list add(j+1, newcomer)
return true
}
j -= 1
}
// no access, we can free it after scope
return addAfterInScope(marker, newcomer)
}
i -= 1
} false
}

access是一个判断当前变量有没有被使用过的函数,我们需要处理很多情况,就像这样:

    if(node instanceOf?(FunctionCall)){
for(i in node as FunctionCall args){
if(accessed(i, v)) return true
}
} else if(node instanceOf?(VariableAccess)){
if(node as VariableAccess getRef() && node as VariableAccess getRef() instanceOf?(VariableDecl) && node as VariableAccess getRef() as VariableDecl name == v name){
return true
}
} else if(node instanceOf?(Scope)){
for(i in node as Scope list){
if(accessed(i, v)) return true
}
} else if(node instanceOf?(ControlStatement)){
for(i in node as ControlStatement body){
if(accessed(i, v)) return true
}
}

最终,这个功能完成了。按道理,这个问题会被解决。当我们试着用修正后的编译器时,问题来了:我们得到的依然是Segmentation Fault。

为什么?

问题来自与指针,因为一个闭包可以间接的走出当前的Scope。比如

foo := func(){ // closure A
} mystructB := struct new(foo, varia) //mystructB go out from scope

这里,mystruct是个结构体,我们不能阻止它走出scope,但它携带了闭包,也就是说我们要阻止他直接拷贝,这是很难的事情。另外一个办法是阻止它走出scope,当然,有一个看起来很合理的规则:

  • 如果一个指针从A赋值到了B,那么它的内容的所有权要从A转移到B。

看起来有点眼熟? 对,这就是Rust。

好吧,让我们来想想如何实现Rust的推断系统——分析其里每一个Node都要有Owner,然后我们要在每一次resolve里都谨慎的判断owner是不是在改变,然后判断它有没有被返回,有没有被拷贝,有没有被当作initializer里的参数…… 这可能要重写编译器50%的代码。

好吧,我放弃了。

Complexity Behind Closure的更多相关文章

  1. Closures in OOC

    Closures in OOC 接上一篇Complexity Behind Closure,这次来专注于Rock是如何在C里实现Closure的. 这篇文章同时发布在Github上. Block as ...

  2. 深入浅出JavaScript之闭包(Closure)

    闭包(closure)是掌握Javascript从人门到深入一个非常重要的门槛,它是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现.下面写下我的学习笔记~ 闭包-无处不 ...

  3. closure

    什么是闭包?百度的答案: 闭包是指可以包含自由(未绑定到特定对象)变量的代码块:这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)."闭包&quo ...

  4. 使用Google Closure Compiler高级压缩Javascript代码注意的几个地方

    介绍 GCC(Google Closure Compiler)是由谷歌发布的Js代码压缩编译工具.它可以做到分析Js的代码,移除不需要的代码(dead code),并且去重写它,最后再进行压缩. 三种 ...

  5. JavaScript闭包(Closure)

    JavaScript闭包(Closure) 本文收集了多本书里对JavaScript闭包(Closure)的解释,或许会对理解闭包有一定帮助. <你不知道的JavsScript> Java ...

  6. 浅析匿名函数、lambda表达式、闭包(closure)区别与作用

    浅析匿名函数.lambda表达式.闭包(closure)区别与作用 所有的主流编程语言都对函数式编程有支持,比如c++11.python和java中有lambda表达式.lua和JavaScript中 ...

  7. 【转】深入浅出JavaScript之闭包(Closure)

    闭包(closure)是掌握Javascript从人门到深入一个非常重要的门槛,它是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现.下面写下我的学习笔记~ 闭包-无处不 ...

  8. 学习Javascript闭包(Closure)

    闭包作用 1.让变量驻留在内存中 2.函数外部可以读取函数内部的私有变量 <!DOCTYPE html> <html lang="en"> <head ...

  9. JavaScript闭包(Closure)学习笔记

    闭包(closure)是JavaScript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现. 下面就是我的学习笔记,对于JavaScript初学者应该是很有用的. 一.变量的作用域 要理解 ...

随机推荐

  1. Swift编程语言学习12 ——实例方法(Instance Methods)和类型方法(Type Methods)

    方法是与某些特定类型相关联的函数.类.结构体.枚举都能够定义实例方法:实例方法为给定类型的实例封装了详细的任务与功能.类.结构体.枚举也能够定义类型方法:类型方法与类型本身相关联.类型方法与 Obje ...

  2. MVC5+EF6 入门

    MVC5+EF6 入门完整教程九   前一阵子临时有事,这篇文章发布间隔比较长,我们先回顾下之前的内容,每篇文章用一句话总结重点. 文章一 MVC核心概念简介,一个基本MVC项目结构 文章二 通过开发 ...

  3. selenium之多线程启动grid分布式测试框架封装(二)

    五.domain类创建 在domain包中创建类:RemoteLanchInfo.java 用来保存启动信息. package com.lingfeng.domain; public class Re ...

  4. CF - 96D - Volleyball

    题意:一个无向图,有n个点,m条边,每条边有距离w,每个点有两个属性(1.从这点出发能到的最远距离,2.从这点出发的费用(不论走多远都一样)),一个人要从点x到点y,问最小费用是多少. 题目链接:ht ...

  5. Oracle的SOME,ANY和ALL操作

    平时很少用的这几个操作,今天遇到了.于是又看了一下文档. SOME和ANY一样,是比较宽松的,类似于OR.满足其中任何一个都可以. ALL要求严格一些,类似于AND,必须全部满足才可以. 不能单独使用 ...

  6. AngularJS+requireJS项目的目录结构设想

    AngularJS+requireJS项目的目录结构设想 准备用AngularJS + require.js 作为新项目的底层框架,以下目录结果只是一个初步设想: /default    放页面,不过 ...

  7. [转载+实践理解]Android动画---如何正确使用平移动画(关于fillBefore和fillAfter的一点说明)(转载)

    红色部分为自己的实践理解 如何实现将View向上平移自身高度一半的距离? TranslateAnimation translate = new TranslateAnimation( Animatio ...

  8. [转载]LVS快速搭建教程

    LVS配置教程 作者:oldjiang 一.前言 相信专程来读此文的读者对LVS必然有一定的了解,首先看图: 毋庸置疑,Load Balancer是负载调度器,由它将网络请求无缝隙调度到真实服务器,至 ...

  9. effective java读书小记(一)创建和销毁对象

    序言 <effective java>可谓是java学习者心中的一本绝对不能不拜读的好书,她对于目标读者(有一点编程基础和开发经验)的人来说,由浅入深,言简意赅.每一章节都分为若干的条目, ...

  10. robotlegs2.0框架实例源码带注释

    robotlegs2.0框架实例源码带注释 Robotlegs2的Starling扩展 有个老外写了robotleges2的starling扩展,地址是 https://github.com/brea ...