2. 面向对象的脚本语言的类的实现
只要是一个对象就要有一个ObjHeader结构体, 该结构体位于该对象的开头
ObjHeader结构
// 以Obj开头的一般为对象, 但是这里ObjHeader仅仅是一个对象头, 不是一个对象, 发现一个规律
// 在结构体中, 如果有定义一个什么type类型的, 则在该脚本语言中就不会定义成对象
typedef struct ObjHeader {
ObjType type; // 对象类型
bool isDark; // 是否可以到达, 如果可以到达, 则GC回收对象
Class *class; // 指向类对象, 在类对象中保存着方法, 这样该对象就可以调用方法了:
struct ObjHeader *next; // 用于链表
} ObjHeader;
// 对象类型
typedef enum ObjType {
ObjTypeList,
ObjTypeMap,
ObjTypeModule,
ObjTypeString,
ObjTypeRange,
ObjTypeFunction,
ObjTypeThread,
ObjTypeClass,
ObjTypeInstance
} ObjType;
Value结构体(Value不是对象, 他在脚本语言层面是一个引用, 因为没有类型, 但是在C语言中需要Value保存属性)
// 它类似于Python中的引用, 在栈中定义, 所以脚本语言模拟的栈就是Value数组, 对象在堆中创建
typedef struct Value {
ValueType type;
union {
double num;
ObjHeader *obj_header;
};
} Value;
// 定义的类型是直接在引用右侧写出来的
// num, true, false这些都能在右侧直接写出来, 而不需要使用其他方法调用
typedef enum {
ValueTypeUndefined,
ValueTypeNull,
ValueTypeObj,
ValueTypeNum,
ValueTypeTrue,
ValueTypeFalse
} ValueType;
// 通过宏将ValueType与Value结构体直接的转换更快捷
Class类对象结构体
/*
好好想一下, 一个类中都有什么, 这与我们在Java和C++编程的类不同, 我们只找所有的类的共同点
1. 对象头
2. 字段个数
3. 方法对象区, 用于存方法
*/
typedef struct Class {
ObjHeader obj_header;
struct Class *superclass;
int field_num;
MethodBuffer methods;
ObjString name;
} Class;
typedef struct Method {
MethodType type;
union {
// C语言实现的方法
Primitive prim_fn;
// 脚本语言将代码编译成ObjClosure对象
ObjClosure *obj;
};
} Method;
typedef num MethodType {
MethodTypeNull,
MethodPrimitive,
MethodScript,
MethodCall // 用于重载
} MethodType;
在构建出上述一个类关系之后, 首先应该定义字符串类(ObjString)
// 这里仅仅是定义了字符串对象, obj_header指向是ObjString类对象
typedef struct ObjString {
ObjHeader obj_header;
long hash_code; // 保存hash值
int len;
char *start[0];
} ObjString;
// 计算字符串的hashcode
int hash_string(const char *str, int length) {
int hashcode = xxxxxxxx;
int idx = 0;
while (idx < length) {
hashcode ^= str[idx++];
hashcode *= yyyyyyyy;
}
return hashcode;
}
元对象
typedef struct {
ObjHeader obj_header;
StringBuffer module_var_name;
ValueBuffer module_var_value;
ObjString *name;
} ObjModule; // 模块不属于任何类, 所有它的obj_header中的class指着指向NULL
typedef struct {
ObjHeader obj_header;
Value field[0]; // 存储属性, 为引用, 这里是在内存中的样子
} ObjInstance;
在脚本中执行过程中最重要的就是代码(存放逻辑的地方, 函数, 方法, 模块中都是)
注意: 接下来的对象结构会比较复杂, 请大致浏览一遍, 在后面会总结他们的关系
- 统一使用ObjFunc表示还这些代码指令
typedef struct ObjFunc {
ObjHeader obj_header;
ByteBuffer instr_stream; // 保存编译后的代码指令, 这是ObjFunc对象的核心功能
ValueBuffer constants; // 常量, 在模块中会有
Module *mod; // 属于哪个模块
int max_stack_size; // 可用的最大栈个数
int upvalue_num; // 用到外层函数变量的个数, 其中upvalue是一个闭包对象, 对在外层函数中栈中的被内层嵌套函数引用到的引用(Value)的封装[为什么? 因为对象在堆中, Value这种应用类型才在栈中:-)], 可以将upvalue看成一个容器, 里面维护着Value类型的值
// 发现在ObjFunc中没有与其对应的upvalue产生联系, 在后面提到的ObjClosure对象中会进行关联
int arg_num;
} ObjFunc;
- 与ObjFunc对象相关的与闭包有关的对象结构
typedef struct ObjUpValue {
ObjHeader obj_header;
Value *ptr; // 指向在外层函数中栈中的局部变量
Value closed_value; // 如果外层函数生命周期结束, 则会回收栈, 为了实现闭包, 将ptr指向的值拷贝到closed_value中即可
} ObjUpValue;
typedef struct ObjClosure {
ObjHeader *obj_header;
ObjFunc *func;
// 在这里对func与他的upvalue进行了关联
ObjUpvalue *upvalues[0];
} ObjClosure;
函数要运行就需要一个环境, 这个环境就是一个栈帧(Frame)
// Frame就是一个函数调用框架, 就是一个栈, 但是又是有一点抽象的, 它通过start_stack来访问Value数组
typedef struct Frame {
int *ip; // 模拟CPU的CS:IP
Value *stack_start;
/* 在上面我们提到了很多的结构体对象, 有ObjFunc, ObjUpvalue, ObjClosure, 那么到底那个才是接口, 这里Closure最大, 所以Closure是接口, 在Method结构体对象中可以看到, 在union中primitive与closure是并列的*/
ObjClosure *closure;
}Frame;
关系总结
- Frame获取到ObjClosure, 得到ObjFunc中的intr_stream执行指令
提到了这么多的结构体, 那么创建他们的顺序是怎样的呢
创建vm目录
typedef struct vm { Parser *cur_parser; // 当前vm使用的parser uint32_t allocated_bytes; // 记录已经分配的内存空间 ObjHeader *all_objects; // 是所有ObjHeader连接成的链表的头 StringTable all_method_names; // 存放方法的所有名称, 因为从用户中读取到一个对象要调用一个方法, 这个是字符串的层面, 我们需要构建出一张符号表, 通过查找该字符在表中的index, 对应的映射到methods中index调用方法 ObjMap *allModules; // 通过map管理名称与模块 ObjThread *cur_thread; // vm支持多线程, cur_thread表示当前的线程(用户态下就是协程) // 所有内置类的类对象指针都放在这里 Class *class_class; // 指向类的类, 是所有元类的基类和元类, 这个需要记住, class_class的元类就是他自己 Class *object_class; // 除了元类, 是所有类的基类, object_class也是class_class的基类, object_class没有基类 Class *string_class; Class *list_class; Class *range_class; Class *thread_class; Class *map_class; /* 下面三个类他们的实现与其他不同, 他们会比较简单, 也没有必要通过复杂的对象来创建 */ Class *num_class; Class *null_class; Class *bool_class; } VM;
- 创建object目录
- 在obj_header.h中创建ObjType枚举, ObjHeader结构体, ValueType枚举, Value结构体
// 对象类型 typedef enum ObjType { ObjTypeList, ObjTypeMap, ObjTypeModule, ObjTypeString, ObjTypeRange, ObjTypeFunction, ObjTypeThread, ObjTypeClass, ObjTypeInstance } ObjType; typedef struct ObjHeader { ObjType type; // 对象类型 bool isDark; // 是否可以到达, 如果可以到达, 则GC回收对象 Class *class; // 指向类对象, 在类对象中保存着方法, 这样该对象就可以调用方法了: struct ObjHeader *next; // 用于链表 } ObjHeader; // 定义的类型是直接在引用右侧写出来的 // num, true, false这些都能在右侧直接写出来, 而不需要使用其他方法调用 typedef enum { ValueTypeUndefined, ValueTypeNull, ValueTypeObj, ValueTypeNum, ValueTypeTrue, // true和false主要用于map中的开放定制法 ValueTypeFalse } ValueType; // 它类似于Python中的引用, 在栈中定义, 所以脚本语言模拟的栈就是Value数组, 对象在堆中创建 typedef struct Value { ValueType type; union { double num; ObjHeader *obj_header; // obj_header的实体在对象中, 这里只需要指向对象头即可 }; } Value; // 通过宏将ValueType与Value结构体直接的转换更快捷 // 此外还要定义Value之间比较的函数 valueIsEquals 思路: Value的类型不同则false Value的类型相同且为数字, 则直接比较数字 Value的类型相同都为Obj, 则比较里面的ObjHeader的类型, 如果相同则再看ObjHeader的类型是什么, 只能比较字符串, range和Class对象, 因为Class有类名属性, 就相当于比较字符串
紧接着创建类对象, 创建class.h文件
/*
好好想一下, 一个类中都有什么, 这与我们在Java和C++编程的类不同, 我们只找所有的类的共同点
1. 对象头
2. 字段个数
3. 方法对象区, 用于存方法
*/
typedef struct Class {
ObjHeader obj_header; // 类也是对象, 所以也会有ObjHeader, 但是其中的ObjHeader的class是指向元类的
ObjString name; // 类名
struct Class *superclass;
uint32_t field_num;
MethodBuffer methods; // 存储Method结构体, 主要封装了方法指针
} Class;
newVM的使用需要创建出核心模块coreModule, 并将其添加到allModules的map中
typedef num MethodType {
MethodTypeNull,
MethodPrimitive,
MethodScript,
MethodCall // 用于重载
} MethodType;
typedef struct Method {
MethodType type;
union {
// C语言实现的方法
Primitive prim_fn;
// 脚本语言将代码编译成ObjClosure对象, ObjClosure包含ObjFunc对象, ObjFunc又有指令流
ObjClosure *obj;
};
} Method;
在有了类, 对象头的基础上, 紧接着创建脚本语言第一个内置对象String, 在obj_string.h中
```c
// 这里仅仅是定义了字符串对象, obj_header指向是ObjString类对象
typedef struct ObjString {
ObjHeader obj_header;
long hash_code; // 保存hash值
int len;
char *start[0];
} ObjString;
// 在创建字符串的时候, 传入const char *s, 使用memset拷贝过来, 不要直接用, 可能会有问题
// 计算字符串的hashcode
long hash_string(const char *str, int length) {
int hashcode = xxxxxxxx;
int idx = 0;
while (idx < length) {
hashcode ^= str[idx++];
hashcode *= yyyyyyyy;
}
return hashcode;
}
// 将计算的hash值保存到ObjString对象中
void hashObjString(ObjString &objString) {
objString->hash_code = hash_string(objString->start, objString->len);
}
```
有了第一个ObjString对象之后, 紧接着考虑元对象的创建, 元对象包括ObjModule和ObjInstance, ObjModule不属于任何类, 同时需要执行一个modname, 所以需要ObjString对象, 这就是为什么需要先创建ObjString对象的原因
typedef struct objmodule { ObjHeader obj_header; // 因为mod不属于任何类, 所有它里面的ObjHeader中的cls为NULL StringBuffer module_names; // 与module_values的长度一样, 用于映射, 因为变量有名字和值 ValueBuffer module_values; ObjString *modname; } ObjModule; typedef struct objinstance { ObjHeader *obj_header; Value fields[0]; // 存放属性的 } ObjInstance;
创建复杂的函数有关的对象, 创建obj_func.h文件
// Class对象为fnClass
typedef struct objfunc {
ObjHeader obj_header;
ByteBuffer inst_stream;
ValueBuffer constants;
int arg_num;
int upvalue_num;
int max_stack_size;
ObjModule *mod;
} ObjFunc;
typedef struct objupvalue {
ObjHeader obj_header;
Value *local_var_ptr;
Value closed_var;
struct objupvalue *next;
} ObjUpvalue;
// class对象也为fnClass
typedef struct objclosure {
ObjHeader obj_header;
ObjFunc *func;
ObjUpvalue *upvalue[0]; // 指向一个ObjUpvalue数组
} ObjClosure;
// 会让线程对象调用
typedef struct frame {
int ip;
ObjClosure *obj_closure;
Value *stack_start;
} Frame;
注意
- Value非常的重要, 在之后函数与方法的实现都是以Value为参数和返回值得, 可以类比于Python, 但是定义一个对象的时候就不需要了, 直接一个对象上去即可, 如ObjString *objString.