v8::Local<v8::Value> 引发的思考

image

V8LocalValueFromJsValue

v8::Localv8::Value 是 v8 和 Node-Api 中十分常见的一种类型。Local 创建了一个指向 js 对象的本地引用, 如下的代码可通过 V8LocalValueFromJsValue 函数从一个 js 对象中返回一个 Local 对象

1
2
3
4
5
6
7
// src/js_native_api_v8.h

inline v8::Local<v8::Value> V8LocalValueFromJsValue(napi_value v) {
v8::Local<v8::Value> local;
memcpy(static_cast<void*>(&local), &v, sizeof(v));
return local;
}

刚开始看见这个代码比较疑惑, 因为对于一个没有经过 new 关键词生成的 local 实例, 其内存是分配在栈中, 类似于一个结构体

  1. 为何在离开 V8LocalValueFromJsValue 函数作用域后没有被自动释放内存, 返回一个结构体的函数是合法的吗?
  2. 还是因为这里是 inline 关键词的作用, 或许是因为 inline 是类似于宏定义文本替换才导致这样书写也是成功的?

验证

于是写了如下的 demo 开始验证, 当 getTest 函数返回一个结构体时会有如何的表现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

typedef struct
{
int age;
} Test;

Test getTest()
{
Test test;
test.age = 100;
printf("getTest test age %d", &test.age);
return test;
}

int main()
{
Test test = getTest();
printf("main test age %d", &test.age);
return 0;
}

最后程序是能正常运行的, 从运行结果来看, 两个 age 字段的地址是不同的

这样我大概得出了 getTest 函数中有一个临时性结构体 test,test 也确实会在 getTest 函数返回时被释放,但由于 test 被当做“值”进行返回,因此编译器将保证 getTest 的返回值对于 getTest 的调用者(caller)来说是有效的, 所以调用者 main 函数里面的 test 将得到一份被复制的数据, 于是表现出相同的 age 字段地址其实是不一样的

1
2
➜  c ./a.out 
getTest test age -385596360main test age -385596328%

那接下来继续再验证一下如果是 inline Test getTest() 的话, 两个字段的地址会不会是一样了 ?

答案是加上了 inline 后 age 字段地址还是不一样的。这样我开始明白了 inline 不是像宏定义那样进行的简单的文本替换, 于是单独学习了一下 inline 函数, 总结如下, 具体推荐阅读文章 C++ 内联函数 inline

  • 宏是由预处理器对宏进行替换的,而内联函数是通过编译器控制实现的。
  • 宏调用并不执行类型检查甚至连正常参数也不检查,但是函数调用却要检查。
  • C语言的宏使用的是文本替换,可能导致无法预料的后果
  • 在宏中的编译错误很难发现,因为它们引用的是扩展的代码,而不是程序员键入的

最后的验证, 如果 getTest 返回的是指针了 ?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

typedef struct
{
int age;
} Test;

Test* getTest()
{
Test test;
test.age = 100;
printf("getTest test age %d", &test.age);
return &test;
}

int main()
{
Test* test = getTest();
printf("main test age %d", &test->age);
return 0;
}

由 demo 运行结果可见, age 字段的地址是一致的。说明平时我们在写代码时尽量不要传递结构体等实体, 因为将会花费一定的时间去复制数据, 而返回指针则会快捷很多

1
2
➜  c ./a.out 
getTest test age -277670872main test age -277670872%

到这里其实我还有最后一个疑惑的点, Local 创建了一个指向 js 对象的本地引用, 那么为何上面的 V8LocalValueFromJsValue 却是复制了一份数据而非引用关系了 ?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// v8/include/v8-local-handle.h

template <class T>
class Local {
public:
V8_INLINE Local() : val_(nullptr) {}
template <class S>
V8_INLINE Local(Local<S> that) : val_(reinterpret_cast<T*>(*that)) {
/**
* This check fails when trying to convert between incompatible
* handles. For example, converting from a Local<String> to a
* Local<Number>.
*/
static_assert(std::is_base_of<T, S>::value, "type check");
}

T* val_;
// ...
}

于是只能查看了一下 v8 中关于 Local 的定义, Local 有一个 val_ 属性, 是一个指针数据, 此时我猜测 V8LocalValueFromJsValue 函数中使用 memcpy 复制数据时, 如果遇见了指针类型, 只会复制一下地址, 所以新的 local 对象持有的 val_ 引用的是原 js 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
#include <string.h>

typedef struct
{
int age;
} my_local_value;

typedef struct
{
int age;
my_local_value *_val;
} my_local;

int main()
{
my_local_value local_value;

my_local local1;
my_local local2;

local1._val = &local_value;

memcpy(&local2, &local1, sizeof(local1));

printf("is_eq: %d \n", local2._val == local1._val ? 1 : 2);

return 0;
}

于是写了上面的验证 demo, 运行结果也证实了 _val 值的是相等的

1
2
➜  c ./a.out
is_eq: 1

小结

1
2
3
4
5
6
7
8
9
10
11
12
13
// v8/include/v8-local-handle.h

/**
* An object reference managed by the v8 garbage collector.
*
* All objects returned from v8 have to be tracked by the garbage collector so
* that it knows that the objects are still alive. Also, because the garbage
* collector may move objects, it is unsafe to point directly to an object.
* Instead, all objects are stored in handles which are known by the garbage
* collector and updated whenever an object moves. Handles should always be
* passed by value (except in cases like out-parameters) and they should never
* be allocated on the heap.
*/

v8::Localv8::Value 既是非常常见也是非常重要的一个概念, 后面需要继续深入探究一下其实现与原理