`
RednaxelaFX
  • 浏览: 3049345 次
  • 性别: Icon_minigender_1
  • 来自: 海外
社区版块
存档分类
最新评论

TJS2中对象的表示方法,其代表的运行时环境,与闭包的关系

阅读更多
对一个对象实例调用(string)转换时,可能会看到这样的结果: (object 0x01AAD840:0x01A9EEC4)
object到string的转换可以通过显式或隐式方式调用.这个转换在tjsVariant.cpp中实现.

\kirikiri2\src\core\tjs2\tjsVariant.cpp
void tTJSVariant::ToString()
{
    switch(vt)
    {
    case tvtVoid:
        String=NULL;
        vt=tvtString;
        break;

    case tvtObject:
      {
        tTJSVariantString * string = TJSObjectToString(*(tTJSVariantClosure*)&Object);
        ReleaseObject();
        String = string;
        vt=tvtString;
        break;
      }

    case tvtString:
        break;

    case tvtInteger:
        String=TJSIntegerToString(Integer);
        vt=tvtString;
        break;

    case tvtReal:
        String=TJSRealToString(Real);
        vt=tvtString;
        break;

    case tvtOctet:
        TJSThrowVariantConvertError(*this, tvtString);
        break;
    }
}

tTJSVariantString * TJSObjectToString(const tTJSVariantClosure &dsp)
{
    if(TJSObjectTypeInfoEnabled())
    {
        // retrieve object type information from debugging facility
        tjs_char tmp[256];
        TJS_sprintf(tmp, TJS_W("(object 0x%p"), dsp.Object);
        ttstr ret = tmp;
        ttstr type = TJSGetObjectTypeInfo(dsp.Object);
        if(!type.IsEmpty()) ret += TJS_W("[") + type + TJS_W("]");
        TJS_sprintf(tmp, TJS_W(":0x%p"), dsp.ObjThis);
        ret += tmp;
        type = TJSGetObjectTypeInfo(dsp.ObjThis);
        if(!type.IsEmpty()) ret += TJS_W("[") + type + TJS_W("]");
        ret += TJS_W(")");
        tTJSVariantString * str = ret.AsVariantStringNoAddRef();
        str->AddRef();
        return str;
    }
    else
    {
        tjs_char tmp[256];
        TJS_sprintf(tmp, TJS_W("(object 0x%p:0x%p)"), dsp.Object, dsp.ObjThis);
        return TJSAllocVariantString(tmp);
    }
}

注意到,上面例子(object 0x01AAD840:0x01A9EEC4)中的两个数字,前者是dsp.Object,后者是dsp.ObjThis. tTJSVariantClosure的这两个成员是由tTJSVariantClosure_S继承而来.

tTJSVariantClosure_S的定义如下:
struct tTJSVariantClosure_S
{
	iTJSDispatch2 *Object;
	iTJSDispatch2 *ObjThis;
};

由此得知,两个数字皆是地址(指针自身的内容都是地址).
我现在还不是太确定Object成员是不是在编译到中间代码的时候就已经构造出了实例,但从多个同类型的对象实例共享一个Object成员的实例来看,应该不是到脚本执行的时候才实例化的.现在虽然找到了几个关键点(T_NEW, VM_NEW, CreateNew, TJS_GET_VM_REG等),但面向对象程序的代码就是很难通过静态分析完全确定动态行为,我仍然理不出运行时行为的头绪.糟糕的是我暂时没办法以debug模式把TJS2的解释器正确编译出来,要调试也很麻烦.

Anyway,还不确定的就先放一边,来看看我确定的部分.上面提到了,每个tTJSVariantClosure实例都会有ObjectObjThis指针为成员.并且,tTJSVariantClosure拥有由iTJSDispatch2接口类型所拥有的各方法.在调用这些方法时,需要经由tTJSVariantClosure来访问,以确保执行时的上下文的正确性.

简单点说,TJS2中每个对象实例都有"两个指针",一个是Object,指向类中的程序代码(如初始化用的代码和函数等),由同类型的所有类型所共享,无状态;另一个是ObjThis,也就是TJS2中以"this"访问可以得到的对象,保存着实例自身的上下文(context,也可以理解为环境environment),不与其它实例共享,用于保持状态信息.在TJS2中,将这个上下文称为闭包(closure).可以参考KCDDP翻译的TJS2中文文档中"类(class)"一节.这里"闭包"可以理解为状态(主要是成员变量)与一个由"类"的语法作用域所形成的范围发生绑定的现象.这样,这个范围就对其中的成员变量形成了闭包.注意到函数也同样可以被赋值给变量,并且函数声名与将函数表达式赋值给一个变量近似等价,因此成员函数(作为变量)也是上下文的一部分.拥有一个专门指向上下文的指针,就意味着绑定可以在运行时改变.使用incontextof运算符就能做到"可调用对象"(Callable, 这里具体指类定义或函数)的上下文的显式指定.

下面的代码可以清晰的展示这一特性:
class A {

    var value = "A";
    
    function printField() {
        System.inform(value);
    }
    
    function printFunc() {
        print2();
    }
    
    function print2() {
        System.inform("A.print2");
    }
}

class B {

    var value = "B";
    
    function print2() {
        System.inform("B.print2");
    }
}

var a = new A();
var b = new B();
(a.printField incontextof b)(); // 使用incontextof运算符显式改变上下文
(a.printFunc incontextof b)();  // 这两行调用的上下文被替换为与b的相绑定
a.printField();  // 正常的方法调用
a.printFunc();   // 这两行调用的上下文都与a的绑定
// 运行结果: 依次显示B, B.print2, A, A.print2


要注意的是,TJS2里的"闭包"与一般支持嵌套定义函数的编程语言中所指的闭包并不相同;TJS2里"闭包"的用法相当独特,特别不应与JavaScript中所支持的闭包所混淆.
在一般的编程语言中,"闭包"的概念是如何会出现的呢? 下面简单解释一下.下面一段会混用"过程""函数""方法"等几个名词,请自行注意分辨区别.也可以查阅wikipedia上的相关条目.该条目原本有些描述不太对(缺乏对命令式语言中闭包的考虑),现在已经订正了一些.
根据《编译原理与实践》一书,可以将基于栈(stack)的运行时环境分为三类:
- 没有局部过程的基于栈的环境
- 带有局部过程的基于栈的环境
- 带有过程参数的基于栈的环境

假如一种语言不允许嵌套声明过程/函数/方法,则所有的函数如果不是局部的就一定是全局的.因而很容易为变量分配空间——全局变量可以放在一个全局区域,而所有函数内的局部变量则直接分配在栈上(或寄存器上).函数总是能访问到它的作用域内的所有变量,外加全局变量.一个函数在被调用的时候,会在栈上压入它的活动记录,在函数结束时销毁;因而所有局部变量将随着函数的退出而被销毁.

int global = 1;

void foo() {
    int local = 2;
    // 此处global与local都能被访问到
}

// 此处就不再能访问到local了

假如一种语言允许嵌套声明过程/函数/方法,但不允许将过程/函数/方法当作参数来传递,前一种运行时环境就无效了.试想下面的伪代码(以C的语义来考虑):
int global = 1;

void foo() {
    int local = 2;

    void goo(int local) {
        hoo();
    }

    void hoo() {
        // 此处能访问到global
        // 但是local呢?
        // 因为hoo()嵌套于foo()之中,我们希望hoo()也能访问到foo()的local
    }
    goo(local);
}

void main(void) {
    foo();
}

这段代码片段中,对hoo()而言global依然是全局变量因而可以正常访问,但local是非局部非全局的变量,不能再按照前面的方式考虑.按照标准的静态作用域则无法在任何活动记录中找到local的信息;如果接受动态作用域,那么可以通过控制链向上找到在goo()中的local.即使goo()中没有local,还可以继续向上找到foo()的local.但这样每次能找到"local"的偏移量都会随着调用的不同而不同,而我们想要的很可能不是这样的.
要解决这问题,仍然可以实现静态作用域.可以用一个访问链(access link)去记录一个函数的包围(enclosing)函数的活动记录,使内部的函数能访问到外部函数的局部变量.在上面的代码里,也就是说hoo()可以访问到foo()的(而不是goo()的)local变量.

前面两种情况都能通过灵活使用栈而得到顺利解决,但下面的这种情况就很难单独依靠栈来维持运行时环境了.假如一个语言允许将过程/函数/方法当作参数来传递,则编译程序无法再像前一种情况生成代码计算调用点上的访问链.为解决非局部引用的问题,函数应包含一个访问指针对,其中一个是代码指针一个是访问链或环境指针(注意到这里跟TJS2的实现的微妙相似与相异).它们通称为闭包(closure),因为访问链"闭合"了由非局部引用引起的"洞"."闭包"的概念来自微积分中λ算子,这里就不深入了.通过闭包,内部函数可以捕捉到外部函数所能访问到的所有引用,包括全局引用和外部函数的局部引用,外加内部函数自身的局部引用;也就是说,内部函数的作用域"捕捉"到了外部的.然而这个内部函数一旦被作为参数传递(例如说被外部函数作为返回值返回),它所能访问到的作用域不能马上被销毁,而要继续存在到没有任何引用继续指向该内部函数为止.这就与前面使用栈的方式相异,很难只用栈来放置活动记录就维持运行时环境.更一般的做法是把闭包相关的活动记录在堆(heap)上分配,然后由垃圾收集器处理销毁的工作.

参考下面代码(ActionScript3):
function add( lhs : int ) : Function {
    return function ( rhs : int ) : int {
        return lhs + rhs;
    }
}

var addFive = add( 5 );
var twelve = addFive( 7 ); // twelve == 12


总之,要特别引起注意的地方是这里引出"闭包"这改变的问题,来自以遵守静态作用域为前提,允许嵌套声明的函数对来自包围它的作用域的非局部非全局引用的访问.
在这个意义上,TJS2并没有实现一般的闭包.在TJS2中可以嵌套定义函数,但无论嵌套多少层,一个函数的上下文总是绑定于离它最近的"类"的作用域中;假如一个函数向嵌套的外层数去,数到头都没有类声明,则它的上下文绑定于全局对象(global).下面的代码将展示这点:
function foo() {
    function goo() {
        this.val = "a string";
    }
    goo();
}

foo();
System.inform(val); // 或者写为global.val都一样.
// 显示a string


幸好吉里吉里2的作者,W.Dee氏已经意识到了这点.在设计TJS2的接班者Risse时,他承诺能实现下面的代码:
function test()
{
    var i = 0;
    return function inc()
    {
        return ++i;
    }
}

var inc_func = test();
Log.message(inc_func()); // => 1
Log.message(inc_func()); // => 2


TJS2不但没实现一般意义的闭包,同时也没有遵循一般意义的静态作用域.TJS2中incontextof运算符可以在静态作用域的基础上将一个可调用对象的上下文重新绑定到任意对象级别上.TJS2解释器会在编译的时候检查是否可执行代码是否有可能调用了非局部(但是是成员的;"全局"在TJS2中是一个特殊的内建对象,全局变量可以认为是global的成员)的引用.如果有的话,则会通过"this proxy"(对应于%-2寄存器)来寻找那些引用.这么做可以说很灵活,但也很容易写出让人难以理解的代码(因为函数与上下文的绑定可能经常变化).建议慎用.
个人倾向于将这种作用域的做法成为"半自动动态作用域".总之关键就是不要以纯静态作用域的假设去阅读TJS2代码,否则一定会吃到苦头...

===========================================

十分有趣的一点是,TJS2中的==与===运算符,分别是由tTJSVariant::NormalCompare与tTJSVariant::DiscernCompare实现的;其中,前者在比较者类型为tvtObject(object)时,比较的是左右操作数的Object.Object;后者在比较者类型为tvtObject(object)时,比较的是左右操作数的Object.Object与Object.ObjThis.这解释了下面代码行为的原因:
class A {
    var someField; // member field
    
    function A() {} // ctor
    
    function foo() {} // member method
}
var a = new A();
System.inform((string)A.foo + (string)a.foo); // 显示两个相同的Object地址
System.inform(A.foo == a.foo); // 1, 即equality相等
System.inform(A.foo === a.foo); // 0, 即identity不相等
System.inform(A.foo === (a.foo incontextof null)); // 1, 即identity也相等
分享到:
评论

相关推荐

    跨数据库同步工具TJS

    2. **示范项目源代码**:这个部分提供了一个实际的项目案例,展示了如何在实际场景中运用TJS进行数据库同步。源代码分析可以帮助用户深入理解TJS的工作原理,以及如何根据需求进行定制化开发。 【标签】中的“跨...

    patch.tjs

    patch.tjs

    tjs.rar_game

    贪吃蛇,这款经典的电子游戏,自诞生以来就深受全球玩家喜爱,其简单的操作和无尽的挑战性使其成为了游戏开发中的一个经典案例。对于想要踏入游戏开发领域或者提升自己编程技能的朋友来说,深入研究“tjs.rar_game”...

    content_1663916621294.tjs

    content_1663916621294.tjs

    PATCH补丁工具 .rar

    2. **动态补丁**:动态补丁是指在程序运行时应用的补丁。这种类型的补丁通常用于修改程序的行为,而无需重新启动软件。它们可能涉及修改内存中的代码或数据,或者利用钩子技术(Hook)来改变函数调用流程。 3. **...

    TJS:以最少的代码实现ExJ

    2. **面向对象**:构造函数、原型链、实例与对象、类(ES6中的class)。 3. **函数式编程**:高阶函数、闭包、函数组合、柯里化等。 4. **异步编程**:回调函数、Promise、async/await。 5. **模块化**:CommonJS、...

    tjs:TJS = tinyc编译器+ quickjs

    TJS = tinyc编译器+ quickjs 混合两个世界的力量,使您可以在js中(内联)调用c函数。 玩具项目,不保证安全,使用风险自负。例子// yes, it support es-moduleimport { Compiler } from "builtin:c" ;// initialize...

    跨数据库同步数据

    在IT行业中,数据库同步是一个重要的任务,特别是在分布式系统和大数据环境中。"跨数据库同步数据"这一主题涉及到如何在不同的数据库之间有效地迁移和更新信息,确保数据的一致性和完整性。在这个过程中,工具和技术...

    club_tjs

    在项目目录中,可以运行: yarn start 在开发模式下运行应用程序。 打开在浏览器中查看它。 如果您进行编辑,则页面将重新加载。 您还将在控制台中看到任何棉绒错误。 yarn test 在交互式监视模式下启动测试运行...

    krkr实现滚动存档界面示例

    在压缩包中的"krkr.exe"是krkr引擎的运行时环境,用于运行使用krkr开发的游戏。"readme.txt"通常包含了项目的说明和指导,例如如何运行示例、注意事项等。"Data"文件夹则可能包含了项目的数据文件,如图像资源、脚本...

    patch安装包

    在Linux操作系统中,由于其开源和高度可定制的特性,WiFi驱动的移植是一项常见的任务,尤其是对于嵌入式设备和物联网(IoT)项目。 Linux的patch系统是通过一系列文本差异来更新源代码树的一种方式。当开发者需要将...

    KRKR进阶实用教程源码 1、捕获鼠标位置

    【标题】"KRKR进阶实用教程源码 1、捕获鼠标位置"涉及的知识点主要集中在使用KRKR引擎进行游戏开发,特别是如何在TJS(Touhou Jockey Script)脚本语言中获取并处理鼠标的实时位置信息。KRKR引擎是一款用于创建 AVG...

    gulp-converter-tjs:将旧的新类型的OpenCV HaarCascade xml数据转换为trackingJs的内部格式

    converter-tjs接受OpenCV训练数据(新的或旧的类型)的XML,将其解析为内部表示形式(JavaScript对象),然后推送以输出其trace.js表示形式。 例子 var gulp = require ( 'gulp' ) ; var converterTjs = require ( ...

    平面埋入TJS激光器

    这种新型的激光器,其特殊的TJS结构,使得它具有稳定的线宽为5kHz,激光频率调谐范围为10GHz,步进调谐可高到70GHz或大约2cm-1。这种激光器的最小阈值电流室温时只有25毫安,高温下可稳定工作几百小时,无劣化现象。...

    krkr_archives:桐木的各种代码

    这是到目前为止我创建的与KiriKiri 2相关的脚本。 KiriKiri 2( ) KiriKiri Z( ) 轮廓如下所示。请参阅每个文件夹中的readme.txt以获取详细说明。 工具文件夹 包含在开发工作中有用的工具。 oggConverter将wav...

    如何将exe程序注册成系统服务的方法.docx

    如何将EXE程序注册成系统服务,以便在计算机启动时自动运行,是一个常见的需求,尤其对于服务器端程序而言。本文将详细介绍使用微软提供的`instsrv.exe`和`srvany.exe`工具来实现这一目标。 首先,`instsrv.exe`是...

    怎样把exe程序注册成系统服务.docx

    特别是在服务器环境中,这样的配置能够确保程序在系统重启之后自动启动,无需人工干预,从而提高系统的稳定性和可用性。接下来,我们将通过一个具体的示例来了解如何实现这一目标。 #### 二、准备工作 首先,需要...

    数字集成电路课件(英文版):chapter10 Timing Issues.ppt

    在图示中,R1和R2代表输入到输出的路径,Cin和Cout表示输入和输出负载电容,而CLK是系统时钟。当数据In改变时,经过R1和R2的延迟,以及Cin和Cout的充电时间,最终影响到输出Out。这个过程必须在时钟CLK的上升沿或...

    及以下供配电系统高压部分PPT学习教案.pptx

    在选择高压电器时,需确保设备能在长期工作电压和过电压情况下正常运行,同时满足一定的技术条件。 首先,对于高压变配电设备的选择,需要依据一系列的规程规范,如《10kV及以下变电所设计规范》GB50053-94、《35~...

Global site tag (gtag.js) - Google Analytics