前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >No.js 中 V8 堆外内存管理和字符编码解码的实现

No.js 中 V8 堆外内存管理和字符编码解码的实现

作者头像
theanarkh
发布2021-10-11 12:18:43
1.2K0
发布2021-10-11 12:18:43
举报
文章被收录于专栏:原创分享原创分享

前言:对于基于 V8 的 JS 运行时来说,堆外内存的管理是非常重要的一部分,因为 gc 的原因,V8 自己管理堆内存大小是有限制的,我们不能什么数据都往 V8 的堆里存储,比如我们想一下读取一个 1G 的文件,如果存到 V8 的堆,一下子就满了,所以我们需要定义堆外内存并进行管理。本文介绍 No.js 里目前支持的简单堆内存管理机制和字符编码解码的实现。

1 字符串的使用

数据的读写,在底层都是一个个字节,那么我们在 JS 层定义的字符串,C++ 层是怎么获取的呢?比如我们在 JS 里调用自定义 log 函数打印日志。

代码语言:javascript
复制
log("hello");

我们来看看 JS 运行时中 log 函数的实现。

代码语言:javascript
复制
void No::Console::log(V8_ARGS) {
    V8_ISOLATE
    String::Utf8Value str(isolate, args[0]);
    Log(*str);}

最终在 C++ 里可以通过 V8 提供的 String::Utf8Value 从 args 中获得 JS 层的字符串,然后调用系统函数把它打印到屏幕就行。但是这种形式使用的内容是 V8 的堆内存。那么如果我们需要操作一个非常大的字符串,那怎么办呢?这时候就需要使用 V8 提供的堆外内存机制 ArrayBuffer。

2 ArrayBuffer 的实现

我们看看这个类关于内存申请的一些实现细节。当我们在 JS 里执行以下代码时

代码语言:javascript
复制
new ArrayBuffer(1)

来看看 V8 的实现。

代码语言:javascript
复制
BUILTIN(ArrayBufferConstructor) {
  // [[Construct]]  args 为 JS 层的参数
  Handle<JSReceiver> new_target = Handle<JSReceiver>::cast(args.new_target());
  // JS 层定义的长度,即 ArrayBuffer 的第一个参数
  Handle<Object> length = args.atOrUndefined(isolate, 1);

  return ConstructBuffer(isolate, 
                         target, 
                         new_target, 
                         number_length, // = length
                         number_max_length,  // 空
                         InitializedFlag::kZeroInitialized);}

接着看 ConstructBuffer 。

代码语言:javascript
复制
Object ConstructBuffer(Isolate* isolate, Handle<JSFunction> target,
                       Handle<JSReceiver> new_target, Handle<Object> length,
                       Handle<Object> max_length, InitializedFlag initialized) {
  	  // resizable = ResizableFlag::kNotResizable
	  ResizableFlag resizable = max_length.is_null() ? ResizableFlag::kNotResizable : ResizableFlag::kResizable;
	  // 申请一个 JSArrayBuffer 对象,不包括存储数据的内存                                              
	  Handle<JSObject> result;
	  ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
	      isolate, result,
	      JSObject::New(target, new_target, Handle<AllocationSite>::null()));
	  auto array_buffer = Handle<JSArrayBuffer>::cast(result);
	
	  size_t byte_length;
	  size_t max_byte_length = 0;
	  // byte_length:需要申请的字节数,由 length Object 解析得到,并且校验申请的大小是否超过阈值
	  if (!TryNumberToSize(*length, &byte_length) ||
	      byte_length > JSArrayBuffer::kMaxByteLength) {
	      // ...
	  }
	  std::unique_ptr<BackingStore> backing_store;
	  // 申请存储数据的内存
	  backing_store = BackingStore::Allocate(isolate, byte_length, shared, initialized);
	  max_byte_length = byte_length;
	  // 保存ArrayBuffer 存储数据的内存
	  array_buffer->Attach(std::move(backing_store));
	  array_buffer->set_max_byte_length(max_byte_length);}

以上代码首先申请了一个 JSArrayBuffer 对象,但是申请的对象中不包括存储数据的内存,接着通过 BackingStore::Allocate 申请存储数据的内存,并且保存到 JSArrayBuffer 中。我们接着看 BackingStore::Allocate 的内存分配逻辑。

代码语言:javascript
复制
std::unique_ptr<BackingStore> BackingStore::Allocate(
    Isolate* isolate, size_t byte_length, SharedFlag shared,
    InitializedFlag initialized) {
  void* buffer_start = nullptr;
  // ArrayBuffer 的内存分配器,初始化 V8 的时候可以设置
  auto allocator = isolate->array_buffer_allocator();
  if (byte_length != 0) {
    auto allocate_buffer = [allocator, initialized](size_t byte_length) {
      void* buffer_start = allocator->Allocate(byte_length);
      return buffer_start;
    };
    // 执行 allocate_buffer 分配内存
    buffer_start = isolate->heap()->AllocateExternalBackingStore(allocate_buffer, byte_length);
  }
  / 分配一个 BackingStore 对象管理上面申请的内存
  auto result = new BackingStore(...);
  return std::unique_ptr<BackingStore>(result);}

我们看到最终通过 allocator->Allocate 分配内存,allocator 是在初始化 V8 的时候设置的,比如 No.js 设置的 ArrayBuffer::Allocator::NewDefaultAllocator()。

代码语言:javascript
复制
v8::ArrayBuffer::Allocator* v8::ArrayBuffer::Allocator::NewDefaultAllocator() {
  return new ArrayBufferAllocator();}

我们看看 ArrayBufferAllocator。

代码语言:javascript
复制
class ArrayBufferAllocator : public v8::ArrayBuffer::Allocator {
 public:
  void* Allocate(size_t length) override {
    return page_allocator_->AllocatePages(nullptr, RoundUp(length, page_size_),
                                          page_size_,
                                          PageAllocator::kReadWrite);
  }
 private:
  PageAllocator* page_allocator_ = internal::GetPlatformDataCagePageAllocator();
  const size_t page_size_ = page_allocator_->AllocatePageSize();};

最终调用 page_allocator_ 去分配内存,从 page_allocator_ 的值 GetPlatformDataCagePageAllocator 我们可以看到这里是调用系统相关的函数去申请内存,比如 Linux 下的 mmap。至此我们看到了 ArrayBuffer 的内存由来,

3 ArrayBuffer 应用

有了 ArrayBuffer,我们就可以在 V8 堆之外申请内存了,我们看看 No.js 里怎么使用。

代码语言:javascript
复制
http.createServer({host: '127.0.0.1', port: 8888}, (req, res) => {
    // HTTP 响应的 body
    const body  = `...`;
    // HTTP 响应报文
    const response = `...`;
    // 申请堆外内存
    const responseBuffer = new ArrayBuffer(response.length);
    // 把响应内容写入堆外内存
    const bytes = new Uint8Array(responseBuffer);
    for (let i = 0; i < response.length; i++) {
        bytes[i] = response[i].charCodeAt(0);
    }
    // 发送给客户端
    res.write(responseBuffer);});

接着我们看看 write 的实现。

代码语言:javascript
复制
// 拿到 JS 的 ArrayBuffer
Local<ArrayBuffer> arrayBuffer = args[1].As<ArrayBuffer>();
std::shared_ptr<BackingStore> backing = arrayBuffer->GetBackingStore();// 申请一个写请求struct io_request *io_req = (struct io_request *)malloc(sizeof(*io_req));memset(io_req, 0, sizeof(*io_req));// 拿到底层存储数据的内存,保存到 request 中等待发送
io_req->buf = backing->Data();
io_req->len = backing->ByteLength();

JS 层设置数据,然后在 C++ 层拿到存储数据的内存发送出去,这个看起来可以满足需求,但是似乎还不够,首先每次都要自己申请一个 ArrayBuffer 和 Uint8Array 比较麻烦,而且还需要自己设置 Uint8Array 的内容,最重要的是 Uint8Array 只能保存单字节的数据,如果我们要发送非单字节的字符就会出现问题了。比如 “𠮷“ 在 JS 里长度是 2,底层占四个字节。

代码语言:javascript
复制
'𠮷'.length => 2

所以还需要封装一个模块处理这些问题。

4 Buffer

类似 Node.js,No.js 也提供 Buffer 模块处理 V8 堆外内存,但是 No.js 没有 Node.js 实现的功能那么多。下面我们看看如何实现。

代码语言:javascript
复制
class Buffer {
    bytes = null;
    memory = null;
    constructor({ length }) {
        this.memory = new ArrayBuffer(length);
        this.bytes = new Uint8Array(this.memory);
        this.byteLength = length;
    }

    static from(str) {
        const chars = toUTF8(str);
        const buffer = new Buffer({length: chars.length});
        for (let i = 0; i < buffer.byteLength; i++) {
            buffer.bytes[i] = chars[i];
        }
        return buffer;
    }

    static toString(bytes) {
        return fromUTF8(bytes);
    }}

使用的方式和 Node.js 一样。

代码语言:javascript
复制
Buffer.from("你好")

字符串通过 Buffer 类实现,Buffer 封装了 ArrayBuffer 和 Uint8Array,不过更重要的是实现了 UTF-8 编码和解码,这样应用层就可以传任何字符串,Buffer 会转成对应的 UTF-8 编码(一系列二进制数据),处理完后再通过底层传输就可以。看一下 UTF-8 编码解码的实现。

代码语言:javascript
复制
function toUTF8(str) {
    // 通过 ... 解决多字节字符问题
    const chars = [...str];
    const bytes = [];
    for (let i = 0; i < chars.length; i++) {
        const char = chars[i];
        const code = char.codePointAt(0);
        if (code > 0 && code < 0x7F) {
            bytes.push(code)
        } else if (code > 0x80 && code < 0x7FF) {
            bytes.push((code >> 6) & 0x1f | 0xC0);
            bytes.push(code & 0x3f | 0x80);  
        } else if ((code > 0x800 && code < 0xFFFF) || (code > 0xE000 && code < 0xFFFF)) {
            bytes.push((code >> 12) & 0x0f | 0xE0);
            bytes.push((code >> 6) & 0x3f | 0x80);
            bytes.push(code & 0x3f | 0x80); 
        } else if (code > 0x10000 && code < 0x10FFFF) {
            bytes.push((code >> 18) & 0x07 | 0xF0);
            bytes.push((code >> 12) & 0x3f | 0x80);
            bytes.push((code >> 6) & 0x3f | 0x80);
            bytes.push(code & 0x3f | 0x80); 
        } 
    }
    return bytes;}

toUTF8 把字符的 Unicode 码变成 UTF-8 编码,具体实现就是根据 UTF-8 的规则,但是有一个地方需要注意的是,不能简单遍历 JS 字符串。比如 “𠮷“ 在遍历的时候情况如下

代码语言:javascript
复制
'𠮷'[0] => '\uD842''𠮷'[1] => '\uDFB7'

所以需要处理一下使得每个字符变得一个独立的元素,再获得它的 unicode 码进行处理。

代码语言:javascript
复制
const chars = [...str];

接着看看 解码。

代码语言:javascript
复制
// 计算二进制数最左边有多少个连续的 1
function countByte(byte) {
    let bytelen = 0;
    while(byte & 0x80) {
        bytelen++;
        byte = (byte << 1) & 0xFF;
    }
    return bytelen || 1;}

function fromUTF8(bytes) {
    let i = 0;
    const chars = [];
    while(i < bytes.length) {
        const byteLen = countByte(bytes[i]);
        switch(byteLen) {
            case 1:
                chars.push(String.fromCodePoint(bytes[i]));
                i += 1;
                break;
            case 2:
                chars.push(String.fromCodePoint( (bytes[i] & 0x1F) << 6 | (bytes[i + 1] & 0x3F) ));
                i += 2;
                break;
            case 3:
                chars.push(String.fromCodePoint( (bytes[i] & 0x0F) << 12 | (bytes[i + 1] & 0x3F) << 6| (bytes[i + 2] & 0x3F) ));
                i += 3;
                break;
            case 4:
                chars.push(String.fromCodePoint( (bytes[i] & 0x07) << 18 | (bytes[i + 1] & 0x3F) << 12 | (bytes[i + 2] & 0x3F) << 6 | (bytes[i + 3] & 0x3F) ));
                i += 4;
                break;
            default:
                throw new Error('invalid byte');
        }
    }
    return chars.join('');}

解码的原理是首先计算单字节的最左边有多少个 1,这个表示后续的多少个字节组成一个字符。计算完后就把一个或多个字节按照 UTF-8 规则拼出 unicode 码,然后使用 fromCodePoint 转成对应字符。最后看看使用例子。

代码语言:javascript
复制
http.createServer({host: '127.0.0.1', port: 8888}, (req, res) => {
    const body  = `<html>
        <head></head>
        <body>
            你好!
        </body>
        </html>`;
    res.setHeaders({
        "Content-Type": "text/html; charset=UTF-8"
    });
    res.end(body);});

主要的改动是返回响应数据给客户端时,直接传字符串就可以,在底层会进行相关处理。

代码语言:javascript
复制
class Socket extends events {
    write(buffer) {
        if (typeof buffer === 'string') {
            buffer = Buffer.from(buffer).getBuffer();
        }
        tcp.write(this.fd, buffer);
    }}

5 总结

目前初步实现了堆外内存管理和编码解码的功能,这样应用层就不需要面对麻烦的堆外内存管理和数据设置问题。另外 V8 堆外内存我们平时可能关注的不是很多,但是却是一个重要的部分。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-10-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 编程杂技 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 字符串的使用
  • 2 ArrayBuffer 的实现
  • 3 ArrayBuffer 应用
  • 4 Buffer
  • 5 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档