十一、添加本机代码

有时候,JavaScript 并不是实现物联网产品的最佳语言。幸运的是,您不需要选择 JavaScript 或 C(或 C++)来构建您的产品:您可以两者都选。C 中的 XS 是由 XS JavaScript 引擎提供的一个低级 C API,这样你就可以将 C 代码集成到你的 JavaScript 项目中(或者将 JavaScript 代码集成到你的 C 项目中!).

以下是在项目中使用本机代码的三个常见原因:

  • 性能——包括 JavaScript 在内的高级语言,在高性能任务上无法胜过优化后的原生代码。您可以添加自己优化的本机函数,并从 JavaScript 代码中调用它们。

  • 访问 硬件特性–作为一种通用编程语言,JavaScript 并不具备对主机硬件独特特性的内置支持。您可以实现自己的函数和类来配置和使用它们。

  • 重用现有的本机代码–您可能有大量适用于您的产品的现有本机代码,并且您不希望必须用 JavaScript 重写。您可以在 JavaScript 项目中使用该代码,方法是在 C 中使用 XS 在它和您的 JavaScript 代码之间架起一座桥梁。

C 中的 XS 允许您使用 C 中的 JavaScript 特性。如您所知,JavaScript 具有 C 不直接支持的功能,如动态类型和对象。在 C 语言中使用 XS 来处理这些特性可能会很笨拙,但是随着您的实践和学习一些常见的模式,它会变得简单明了。本章通过一系列例子介绍了 C 语言中的 XS,这些例子展示了在 JavaScript 和 C 代码之间搭建桥梁的不同技术。

请注意,许多实现高级编程语言的引擎都提供了 API 来桥接该语言和本机代码。Java 语言为此定义了 Java 本地接口(JNI ), V8 JavaScript 引擎提供了一个 C++ API。

Important

本章介绍的信息是一个高级主题。它假设您熟悉 C 语言编程,并且对本书中讨论的基本 JavaScript 概念有坚实的理解。

安装主机

本章不需要安装主机,因为所有本机代码都必须是主机本身的一部分;因此,您将本章中的每个示例都构建为独立的主机。您可以使用mcconfig,而不是使用mcrun来安装示例。下列命令列分别适用於 ESP32 和 ESP8266 目标:

> mcconfig -d -m -p esp32

> mcconfig -d -m -p esp

这些命令行没有指定开发板(例如,esp32/moddable_two),因为这些示例仅使用微控制器的通用特性,并不依赖于开发板的特定特性。

当您使用mcconfig构建示例时,JavaScript 和 C 代码都会被构建。如果构建过程中出现错误,就会报告给命令行。

生成随机整数

第一个本机代码集成的例子生成随机整数。你在第 9 章看到了random-rectangles例子使用了 JavaScript 内置函数Math.random生成的随机数。这个例子效率不高,因为Math.random返回一个浮点值,迫使 Poco 为每个矩形将几个浮点值转换成整数。浮点运算在微控制器上通常很慢,在这里它们没有任何好处。C 标准库的rand函数生成随机整数,$EXAMPLES/ch11-native/random-integer示例从使用rand为 JavaScript 代码生成随机整数开始。

创建本机函数

第一步是创建一个 JavaScript 函数,JavaScript 代码可以调用这个函数来调用 C 函数。random-integer示例在main.js源代码文件中声明了一个randomInt函数。

function randomInt() @ "xs_randomInt";

该语法创建了一个名为randomInt的 JavaScript 函数,当调用该函数时,它会调用本机函数xs_randomInt,本质上是建立了一个从 JavaScript 到 c 的桥梁。这里使用的@不是标准的 JavaScript 语法,而是 XS 提供的一个语言扩展,用于简化向项目添加本机代码。因此,这些代码不太可能与其他 JavaScript 引擎一样编译或运行。

创建函数后,您可以像调用任何其他 JavaScript 函数一样调用它。main.js模块调用它 100 次,跟踪结果到调试控制台。

for (let i = 0; i < 100; i++)
    trace(randomInt(), "\n");

实现本机函数

xs_randomInt的实现包含在main.c中。当您构建一个扩展名为.js的文件时,mcconfig也会构建一个扩展名为.c的同名文件。清单 11-1 显示了main.c的全部内容。

#include "xsmc.h"

void xs_randomInt(xsMachine *the)
{
    xsmcSetInteger(xsResult, rand());
}

Listing 11-1.

include预处理器命令引入 c 语言中 XS 的头文件。(文件名xsmc,代表“XS 微控制器”。))还有一个由一些代码使用的xs.h头文件。这两个头文件提供了相同的功能,但是xsmc.h头文件中的功能更有效,因此更适合在微控制器上使用。

xs_randomInt的原生函数原型用于所有在 C 中使用 XS 实现原生方法的函数。JavaScript 参数不会作为参数传递给 C 函数。在本章的后面你会看到如何访问参数。

这个例子需要返回值——调用rand的结果。rand的结果是一个整数,所以这个例子使用了xsmcSetInteger,一个将本地 32 位整数值赋给 JavaScript 值的函数。这里的 JavaScript 值是xsResult,指的是 JavaScript 栈上函数的返回值。

使用硬件随机数生成器

您已经看到了声明、调用和实现一个简单的本机函数是多么简单。当您运行random-integer示例时,您会看到从 0 到 2,147,483,647 的 100 个随机数被跟踪到调试控制台。但是,当您重新启动微控制器并第二次运行该示例时,您会看到完全相同的数字列表。这不是很随意。为什么会这样?

rand函数是一个伪随机数生成器。这是一种产生随机数字的算法。然而,当您重新启动微控制器时,您也会重新启动伪随机数生成器算法,使其生成相同的数字序列。您可以使用srand功能让算法启动不同的序列,但是您必须在每次重启时为srand提供不同的起始点。初始化序列最常见的方法是使用当前时间。不幸的是,许多微控制器,包括 ESP32 和 ESP8266,在启动时不知道时间,所以这种技术不能应用。

幸运的是,许多微控制器,包括 ESP32 和 ESP8266,都有硬件来生成随机数,这些值比rand生成的值更随机。$EXAMPLES/ch11-native/random-integer-esp示例展示了如何使用硬件随机数生成器。

Important

并非所有的随机数都保证不可预测,可以安全地用于安全解决方案,例如保护网络连接的 TLS 协议。(具有这种保证的随机数被称为密码安全的。)您应该始终验证您使用的随机数来源是否符合项目的安全要求。这不容易做到,但很重要,因为弱随机数生成器是项目整体安全性的一个漏洞。

在 ESP32 上,访问硬件随机数生成器只需将调用rand替换为调用 ESP-IDF 函数esp_randomesp_random提供的随机性程度取决于许多因素,包括无线电(Wi-Fi 或蓝牙)是否启用。

xsmcSetInteger(xsResult, esp_random());

在 ESP8266 上,有一个未记录的硬件随机数生成器,似乎工作得很好。它应该小心使用,因为它的精确特性是未知的。要访问随机数生成器,可以直接读取它的硬件寄存器。

uint32_t random = *(volatile uint32_t *)0x3FF20E44;

清单 11-2 显示了使用本地随机数生成器的修改后的本地实现。因为在 ESP32 和 ESP8266 上对生成器的访问方式不同,所以 C 代码使用条件编译来选择正确的版本,并在为不受支持的目标编译代码时生成错误。

void xs_randomInt(xsMachine *the)
{
#if ESP32
    xsmcSetInteger(xsResult, esp_random());
#elif defined(__ets__)
    xsmcSetInteger(xsResult, (*(volatile unt32_t *)0x3FF20E44));
#else
    #error Unsupported platform
#endif
}

Listing 11-2.

使用此randomInt功能有两个问题:

  • ESP32 和 ESP8266 硬件随机数生成器都返回 32 位无符号值。xsmcSetInteger函数需要一个 32 位有符号值。因此,使用硬件随机数技术会更改 JavaScript randomInt函数的结果,以返回从–2,147,483,648 到 2,147,483,647 的值范围。回想一下,当你使用rand时,所有的值都是正的。您可以使用xsmcSetNumber来返回作为浮点数的无符号 32 位值;然而,这与将随机数作为整数值返回的目标背道而驰。

  • 通常你想要一个一定范围内的随机数,生成一个范围内的值需要除法或者模运算。除法运算通常需要浮点运算,因为结果可能有小数部分。如果两个操作数都是整数,模运算可以使用整数除法。然而,不需要randomInt的调用者有效地将返回值限制在期望的范围内,您可以修改本地函数来实现这一点。

下一节将讨论这些问题。

将随机数限制在一个范围内

$EXAMPLES/ch11-native/random-integer-esp-range示例将随机数限制在一个范围内。第一步是声明一个接受随机值范围的函数。randomIntRange函数接受一个表示随机值范围的参数,从 0 开始,到max结束。

function randomIntRange(max) @ "xs_randomIntRange";

更新main.js中的调用代码以传入范围,在本例中是 1000。

for (let i = 0; i < 100; i++)
    trace(randomIntRange(1000), "\n");

本机函数必须首先检索作为第一个参数传递的范围。使用xsArg通过索引访问参数。参数从 0 开始编号,因此第一个参数作为xsArg(0)被访问。如果调用者没有传递任何参数,xsArg(0)抛出一个异常;因此,本机代码通常不需要检查传递的参数数量。(如果您的函数需要知道参数的数量,请使用xsmcArgc整数值。)XS 在 C 中抛出的异常是普通的 JavaScript 异常,可能会被 JavaScript 代码中熟悉的trycatch块捕捉到。

C 代码不能对参数的类型做出任何假设,因为 JavaScript 不会强制执行任何关于传递给函数的参数类型的规则。C #中的 XS 提供了将 JavaScript 值转换成特定本机类型的函数。在xs_randomIntRange函数(清单 11-3 )中,对xsmcToInteger的调用要求 XS 将 JavaScript 属性转换为有符号的 32 位整数。如果 XS 能够执行转换,它将返回结果;否则,它会抛出一个 JavaScript 异常。例如,传递一个字符串值"100"或一个数字值100.1会成功,因为 JavaScript 知道如何将它们转换成整数;然而,传递一个空对象{}会失败。

void xs_randomIntRange(xsMachine *the)
{
    int range = xsmcToInteger(xsArg(0));
    if (range < 2)
        xsRangeError("invalid range");
    ...
}

Listing 11-3.

本地函数实现接下来验证所请求的范围。小于两个值的范围对于整数随机数没有意义。如果范围无效,函数调用xsRangeError抛出 JavaScript 错误RangeError。前面的 C 代码相当于这几行 JavaScript 代码:

if (range < 2)
    throw new RangeError("invalid range");

在连接 JavaScript 和 C 代码的本机代码中包含错误检查非常重要。JavaScript 程序员希望这种语言是安全的——应该没有办法使设备崩溃或损坏——JavaScript 引擎和运行时尽最大努力实现这一目标。您的本机代码也必须这样做。例如,如果 JavaScript 代码为该范围传递 0,则 C 语言不会定义结果。ESP32 右侧为 0 的模运算产生一个IntegerDivideByZero异常,ESP8266 产生一个Illegal Instruction异常,两者都会复位微控制器。

剩下的xs_randomIntRange(清单 11-4 )的实现很简单。模运算符(%)将随机值限制在指定的范围内,而不是直接返回 32 位无符号整数值。

#if ESP32
    xsmcSetInteger(xsResult, esp_random() % range);
#elif defined(__ets__)
    xsmcSetInteger(xsResult,
                   (*(volatile uint32_t *)0x3FF20E44) % range);
#else
    #error Unsupported platform
#endif

Listing 11-4.

比较随机数方法

原生randomIntRange函数只是几行原生代码,但与内置的Math.random函数相比,这几行代码对于物联网开发具有许多优势:

  • 返回值是整数,而不是浮点,允许在微控制器上更有效地执行。

  • 返回值被有效地限制在请求的范围内。

  • 这些数字更加随机,因为它们使用硬件随机数生成器。

当然,也有缺点:

  • 本机代码是不可移植的。它只在两个微控制器上成功构建。

  • 您必须将本机代码作为宿主的一部分来构建。

  • 本机代码的实现和调试更加复杂,需要额外的专业知识。

当您可以选择将本机功能添加到项目中时,您应该在权衡利弊的基础上做出决定。

BitArray

JavaScript 类型化数组,比如Uint8ArrayUint32Array,使您能够使用最少的内存处理 8 位、16 位和 32 位整数值的数组。BitArray类实现了一个 1 位数组——也就是说,一个只存储值 0 和 1 的数组。这对于有效存储从数字输入接收的大量样本非常有用。

本节介绍了BitArray的两种变体,每一种都有相同的 JavaScript API。第一个使用 JavaScript ArrayBuffer来存储位,而第二个使用 C calloc函数分配的本地内存。

BitArray类构造函数接受一个参数:数组需要存储的位数。该类提供了getset方法来访问数组中的值。清单 11-5 显示了使用BitArray类的测试代码。

import BitArray from "bitarray";

let bits = new BitArray(128);
bits.set(2, 1);
bits.set(3, bits.get(3) ? 0 : 1);

Listing 11-5.

getset的第一个参数是数组中要获取或设置的位的索引。第一个数组元素的索引为 0。该示例的最后一行切换索引 3 处的位的值。

使用由ArrayBuffer分配的内存

清单 11-6 显示了$EXAMPLES/ch11-native/bitarray-arraybuffer示例中BitArray的实现。它从用 JavaScript 声明类开始。与前面的随机整数示例一样,特殊的 XS @语法用于将 JavaScript 函数连接到本地 C 函数。注意,构造函数是用 JavaScript 实现的,而getset方法是用 C 实现的。不要求类完全用 JavaScript 或 C 实现;您可以选择最适合每种方法的语言。

class BitArray {
    constructor(count) {
        this.buffer = new ArrayBuffer(Math.ceil(count / 8));
    }
    get(index) @ "xs_bitarray_get";
    set(index, value) @ "xs_bitarray_set";
}

export default BitArray;

Listing 11-6.

构造函数分配一个ArrayBuffer来保存位值。因为新的ArrayBuffer的存储器总是被初始化为 0,所以不需要进一步的初始化。要存储的位数除以 8,以确定所需的字节数,然后使用Math.ceil四舍五入,以确保当位数不能被 8 整除时,有足够的字节分配。ArrayBuffer被分配给BitArray实例的buffer属性。getset的本地实现使用buffer属性访问内存。

get功能

get函数的本机实现xs_bitarray_get从检索位的索引开始,位是函数的第一个参数。它使用index参数来计算byteIndex,即包含该位的字节的索引,以及bitIndex,即该字节中该位的索引。

int index = xsmcToInteger(xsArg(0));
int byteIndex = index >> 3;
int bitIndex = index & 0x07;

接下来,xs_bitarray_get获取一个指针,指向存储在buffer属性中的由ArrayBuffer分配的内存。为此,它首先在 JavaScript 栈上分配一个临时 JavaScript 变量,通过调用带有参数1xsmcVars来指定临时变量的数量。

xsmcVars(1);

使用xsmcVars分配的变量通过xsVar访问,这类似于xsArg,但是访问的是本地临时变量,而不是函数的参数。当分配变量的本机函数(在本例中为xs_bitarray_get)返回时,变量被自动释放。你应该在一个函数中只调用xsmcVars一次,一次分配所有需要的临时变量。

xs_bitarray_get的实现通过调用xsmcGet来检索对其实例的buffer属性的引用。buffer属性的值放在xsVar(0)中。

xsmcGet(xsVar(0), xsThis, xsID_buffer);

这里的第二个参数xsThis告诉xsmcGet您想要从哪个对象中检索属性。第三个参数,这里的xsID_buffer,指定您想要检索的属性的名称是buffer

前面的步骤在 c 语言中使用了许多来自 XS 的不熟悉的调用。它们在 JavaScript 中非常简单,但在 c 语言中表达起来要冗长得多。与调用xsmcVarsxsmcGet等效的 JavaScript 如下:

let var0;
var0 = this.buffer;

buffer属性不是指向ArrayBuffer实例使用的内存缓冲区的指针;这是对实例的引用。正如使用xsmcToInteger将 JavaScript 值转换成整数一样,使用xsmcToArrayBuffer将 JavaScript 值转换成本机指针。如果 JavaScript 值不是一个ArrayBuffer实例,那么对xsmcToArrayBuffer的调用会抛出一个异常。

uint8_t *buffer = xsmcToArrayBuffer(xsVar(0));

现在xs_bitarray_get有了缓冲区指针,它使用之前计算的byteIndexbitIndex值来读取该位,并将 JavaScript 函数调用的返回值设置为 0 或 1。

if (buffer[byteIndex] & (1 << bitIndex))
    xsmcSetInteger(xsResult, 1);
else
    xsmcSetInteger(xsResult, 0);

set功能

xs_bitarray_setset函数(清单 11-7 的实现与get的实现非常相似。以同样的方式确定byteIndexbitIndexbuffer的值。唯一的区别是第二个参数的值(通过xsArg(1)访问)用于确定是否设置或清除指定的位。

int value = xsmcToInteger(xsArg(1));
if (value)
    buffer[byteIndex] |= 1 << bitIndex;
else
    buffer[byteIndex] &= ~(1 << bitIndex);

Listing 11-7.

安全漏洞

使用由ArrayBuffer分配的内存的BitArray的实现工作得很好,但是它有一个致命的缺陷,使它不适合在实际产品中安全使用。getset函数不验证index参数是否在分配的内存范围内。这使得使用此BitArray实现的代码能够读写嵌入式设备上的任意内存,这可能导致崩溃或被用作隐私攻击的基础。有多种方法可以解决这个问题;下一节将讨论其中之一。

使用由calloc分配的内存

$EXAMPLES/ch11-native/bitarray-calloc示例中BitArray的实现解决了刚才讨论的bitarray-arraybuffer示例所带来的安全问题。它存储构造函数分配的位数,然后根据存储的值验证传递给getset调用的索引。

bitarray-calloc示例中的BitArray实现使用calloc而不是ArrayBuffer来分配内存。这两种方法分配的内存来自两个不同的内存池:由calloc分配的内存来自本机系统内存堆,而由ArrayBuffer分配的内存在由 XS 管理的内存堆内。一些主机在其中一个池中配置了比另一个池更多的可用空间,这可能会影响您决定从哪里分配内存。使用由calloc分配的内存需要的代码要少一些,尽管这种差别可能并不明显。

bitarray-calloc示例展示了将本地代码集成到项目中的一些重要技术。除了一个本地构造函数,这个BitArray类还有一个本地析构函数,当类的一个实例被垃圾收集时执行清理。在 XS,具有本机析构函数的对象被称为宿主对象

类声明

清单 11-8 展示了类声明。与来自bitarray-arraybuffer示例的实现不同,BitArray的实现主要使用本地方法。请注意,实现析构函数的本机 C 函数的名称xs_bitarray_destructor跟在类名声明的后面。

class BitArray @ "xs_bitarray_destructor" {
    constructor(count) @ "xs_bitarray_constructor";
    close() @ "xs_bitarray_close";
    get(index) @ "xs_bitarray_get";
    set(index, value) @ "xs_bitarray_set";
    get length() @ "xs_bitarray_get_length";
    set length(value) {
        throw new Error("read-only");
    }
}

Listing 11-8.

getset方法的声明与前面的例子相同,尽管实现有些不同。

本机构造函数、析构函数和close函数密切相关。接下来的几节将依次讨论这两个问题。

构造函数

清单 11-9 中的本地构造函数开始很像 JavaScript 实现,通过计算存储请求的位数所需的字节数,然后分配这些字节。构造函数分配整数大小的额外空间来保存位数。如果分配失败,构造函数调用xsUnknownError抛出异常。在名称xsUnknownError中使用Unknown意味着这是一个通用错误,它使用 JavaScript Error类,而不是一个特定错误,如RangeError

void xs_bitarray_constructor(xsMachine *the)
{
    int bitCount = xsmcToInteger(xsArg(0));
    int byteCount = (bitCount + 7) / 8;
    uint8_t *bytes = calloc(byteCount + sizeof(int), 1);
    if (!bytes)
        xsUnknownError("no memory");

    *(int *)bytes = bitCount;
    xsmcSetHostData(xsThis, bytes);
}

Listing 11-9.

一旦分配了内存,请求的位数就存储在块的开始处。因为存储器是使用calloc分配的,所以所有位都被初始化为 0。

xsmcSetHostData的调用存储了一个对分配给这个主机对象的内存的引用。然后,通过调用xsmcGetHostData,这个指针对该对象的所有本机方法都可用。您可能想简单地将bytes指针存储在一个全局变量中;然而,当对象有多个实例时,这种方法就失败了,因为两个对象不能共享一个 C 全局变量。使用xsmcSetHostData存储数据指针意味着BitArray的实现支持任意数量的并发实例。

析构函数

这是本书中你第一次看到析构函数。它们在 C++中处理对象时很常见,但在 JavaScript 语言中并不可见。相反,当对象被垃圾收集时,JavaScript 会自动释放它们所使用的内存。JavaScript 引擎不知道如何释放你的主机对象分配的资源,比如用calloc分配的内存。因此,您必须实现析构函数。

对于BitArray,析构函数(清单 11-10 简单的调用free来释放calloc分配的内存。

void xs_bitarray_destructor(void *data)
{
    if (data)
        free(data);
}

Listing 11-10.

下面是实现析构函数时需要注意的一些细节:

  • 析构函数的函数原型不同于常规的本机方法调用。它没有被传递一个对 XS 虚拟机的引用作为the,而是有一个数据指针的参数,与你传递给xsmcSetHostData的值相同。

  • 因为没有对 XS 虚拟机的引用(没有the参数),所以不能在 c 中调用 XS,比如不能调用xsmcGetHostData,这就是为什么数据指针总是传递给析构函数。这也意味着你的析构函数不能创建新的对象,改变属性值,或者对对象进行函数调用。这些限制是必要的,因为当这样的操作不安全时,析构函数是从垃圾收集器内部调用的。

  • data的值可以是NULL。例如,当构造函数中的内存分配失败时,就会发生这种情况。正如您将在下一节看到的,这也发生在调用close方法之后。因此,一个好的做法是,在使用析构函数之前,总是检查一下data参数是否是NULL,就像这个例子一样。

close功能

第三章和第五章包含了拥有close方法的 JavaScript 对象的例子。此方法释放对象拥有的任何本机资源—内存、文件句柄、网络套接字等。如果对象没有被显式关闭,当垃圾收集器确定对象不再被使用时,这些资源最终会被释放。然而,没有办法知道垃圾收集器什么时候会做出决定,这意味着可能需要很长时间才能释放资源。close调用通过为代码提供一种显式释放这些资源的方式来解决这个问题。

许多主机对象都有一个类似于BitArrayclose实现(清单 11-11 )。

void xs_bitarray_close(xsMachine *the)
{
    uint8_t *buffer = xsmcGetHostData(xsThis);
    xs_bitarray_destructor(buffer);
    xsmcSetHostData(xsThis, NULL);
}

Listing 11-11.

下面是这几行代码的作用:

  1. xsmcGetHostData的调用检索在构造函数中分配的数据指针,并通过对xsmcSetHostData的调用与该对象相关联。

  2. 数据指针被传递给析构函数,析构函数负责释放资源。

  3. xsmcSetHostData的调用将保存的数据指针设置为NULL。这确保了如果close被调用两次,数据指针只被释放一次。

getset功能

xs_bitarray_get的实现计算位和字节索引值的方式与getArrayBuffer版本相同:

int index = xsmcToInteger(xsArg(0));
int byteIndex = index >> 3;
int bitIndex = index & 0x07;

如清单 11-12 所示,xs_bitarray_get使用xsmcGetHostData来检索数据缓冲区。如果缓冲区是NULL,则表明实例已经被关闭,get抛出错误。分配的比特数的计数存储在缓冲器的第一个整数中;它被提取到本地变量bitCount,然后缓冲区指针前进指向位数组值。

uint8_t *buffer = xsmcGetHostData(xsThis);
int bitCount;
if (NULL == buffer)
    xsUnknownError("closed");

bitCount = *(int *)buffer;
buffer += sizeof(int);

Listing 11-12.

在访问所请求的位之前,该实现首先检查该值是否在范围内。因为索引是一个有符号的整数,所以它检查它是否大于分配的位数,并且索引不为负。

if ((index >= bitCount) || (index < 0))
    xsRangeError("invalid bit index");

检查完成后,读取所请求的位并设置返回值与之前的版本相同:

if (buffer[byteIndex] & (1 << bitIndex))
    xsmcSetInteger(xsResult, 1);
else
    xsmcSetInteger(xsResult, 0);

set的实现应用了本节中针对get描述的相同变化,因此这里不再重复。

length属性

类型化数组类在它们的实例中包含一个length属性,与Array的实例一样,它指示数组中元素的数量。这个值在迭代数组时很有用。因为BitArray的这个实现存储了分配的位数,所以它也可以提供一个length属性。

length属性是用 getter 和 setter 实现的,这两种特殊的 JavaScript 函数在代码访问属性时被调用。使用length的 getter 和 setter 使您能够编写如下代码,将所有位初始化为 1:

let bits = new BitArray(55);
for (let i = 0; i < bits.length; i++)
    bits.set(i, 1);

实现length属性的第一步是向BitArray类添加 getter 和 setter。这里的 getter 是xs_bitarray_get_length本地函数。length属性是只读的,所以 setter 实现不是本地代码,而是一个总是抛出异常的 JavaScript 函数。请注意,主机对象可能有 JavaScript 方法。

get length() @ "xs_bitarray_get_length";
set length(value) {
    throw new Error("read-only");
}

清单 11-13 中所示的xs_bitarray_get_length的实现非常简单。它使用xsmcGetHostData来检索在构造函数中创建的数据指针。如果实例已经关闭——也就是说,如果bufferNULL——它抛出一个异常;否则,它将返回值设置为从数据指针开始处提取的位计数。

void xs_bitarray_get_length(xsMachine *the)
{
    uint8_t *buffer = xsmcGetHostData(xsThis);
    if (NULL == buffer)
        xsUnknownError("closed");

    int bitCount = *(int *)buffer;
    xsmcSetInteger(xsResult, bitCount);
}

Listing 11-13.

这种方法的优点

使用由calloc分配的内存的BitArray的第二种实现比第一种版本有许多优点:

  • 它验证输入值,消除了草率代码导致崩溃和恶意代码侵犯隐私的可能性。

  • 它提供了一个length属性,使得使用起来更加方便。

  • 它使用系统内存来存储位数据,减少了 JavaScript 引擎管理的内存堆中使用的内存。

  • 它使用 C #中 XS 的主机数据特性来跟踪内存缓冲区,比使用 JavaScript 属性需要的代码更少,运行速度更快。

Wi-Fi 信号通知

您已经学习了如何实现一个类来将本机资源作为宿主对象进行管理。下一个例子展示了如何从 C 代码调用回 JavaScript,以及如何使用字典配置主机对象。这两种技术都被可修改的 SDK 中的许多主机对象所使用。

$EXAMPLES/ch11-native/wifi-rssi-notify示例实现了WiFiRSSINotify类,它允许您注册在 Wi-Fi 信号强度超过或低于指定阈值时调用的回调函数。您可以在您的产品中使用这一点,以向用户指示 Wi-Fi 何时可能运行良好,或者在信号较弱时抑制您生成的网络流量。这个类可以完全用 JavaScript 实现,使用Timer和第三章的“获取网络信息”一节中介绍的net模块。这种使用本机代码的实现更加高效,并且提供了一个方便的起点来展示如何从字典配置您的主机对象以及如何调用回调函数。

当您运行这个示例时,您必须为微控制器指定一个要连接的 Wi-Fi 接入点。这是因为 RSSI 测量你的微控制器和它所连接的接入点之间的信号强度;如果没有联系,就没什么可衡量的。下面是构建和运行此示例的典型命令行:

> mcconfig -d -m -p esp32 ssid="My Wi-Fi" password="secret"

测试代码

WiFiRSSINotify类遵循一种常见的模式,即构造函数接受配置选项的字典对象。清单 11-14 显示了main.js中构建该类实例的测试代码。您需要指定 RSSI 阈值,低于该阈值的信号被视为弱信号,高于该阈值的信号被视为强信号。可选的poll属性配置检查信号强度的频率;在本例中,它被设置为 1000 毫秒。默认轮询频率是 5000 毫秒。

import WiFiRSSINotify from "wifirssinotify";

let notify = new WiFiRSSINotify({
    threshold: -66,
    poll: 1000
});

Listing 11-14.

一旦创建了通知实例,您就可以安装一个onWeakSignal和/或onStrongSignal回调,如清单 11-15 所示。当 RSSI 达到或低于指定的阈值时,调用onWeakSignal回调,当 RSSI 超过阈值时,调用onStrongSignal。这些函数在超过阈值时调用,而不是每次轮询 RSSI 时调用。当前的 RSSI 值被传递给回调函数。

notify.onWeakSignal = function(rssi) {
    trace(`Weak Wi-Fi signal. RSSI ${rssi}.\n`);
}
notify.onStrongSignal = function(rssi) {
    trace(`Strong Wi-Fi signal. RSSI ${rssi}.\n`);
}

Listing 11-15.

WiFiRSSINotify

WiFiRSSINotify的 JavaScript 类只是一个宿主对象,它有析构函数、构造函数和close函数,都是用本机代码实现的:

class WiFiRSSINotify @ "xs_wifirssinotify_destructor" {
    constructor(options) @ "xs_wifirssinotify_constructor";
    close() @ "xs_wifirssinotify_close";
}

用于onWeakSignalonStrongSignal回调的默认函数不是该类的一部分。在调用回调之前,WiFiRSSINotify确认实例有一个与回调同名的属性。

原生RSSINotifyRecord结构

WiFiRSSINotify类需要维护状态来执行它的工作。该状态存储在名为RSSINotifyRecord的 C 语言结构中,如清单 11-16 所示。您可以将这种数据结构视为 JavaScript 实例中属性的 C 等价物。

struct RSSINotifyRecord {
    int         threshold;
    int         state;
    modTimer    timer;
    xsMachine   *the;
    xsSlot      obj;
};

Listing 11-16.

在查看使用这种数据结构的代码之前,回顾一下每个字段的用法是很有帮助的:

  • threshold –RSSI 阈值,低于该阈值信号被认为是弱信号,高于该阈值信号被认为是强信号。

  • state –``WiFiRSSINotify实例总是处于三种状态之一:创建时是kRSSIUnknown,然后是kRSSIWeakkRSSIStrong。该状态用于在状态没有改变时消除多余的回调。

  • 用于实现轮询的本地定时器。

  • the –对包含WiFiRSSINotify实例的 XS 虚拟机的引用。它用于从计时器中调用回调。

  • obj –对用于从计时器调用回调的WiFiRSSINotify对象的引用。这个字段的类型xsSlot,被 XS 用来保存任何 JavaScript 值。您已经知道的xsArgxsVarxsGet函数返回类型xsSlot的值。

以下部分提供了有关如何使用这些字段的更多详细信息。

为了方便起见,该实现还将RSSINotify定义为指向RSSINotifyRecord的指针:

typedef struct RSSINotifyRecord *RSSINotify;

构造函数

WiFiRSSINotify构造函数首先为RSSINotifyRecord结构分配存储空间。一旦这个结构被完全初始化,它就被使用xsmcSetHostData附加到对象上。通常,数据结构在初始化之前不会附加到对象上,以避免在构造函数执行期间发生错误时拥有部分初始化的结构。

RSSINotify rn = calloc(sizeof(RSSINotifyRecord), 1);
if (!rn)
    xsUnknownError("no memory");

接下来,构造函数初始化statetheobj字段:

rn->state = kRSSIUnknown;
rn->obj = xsThis;
rn->the = the;

构造函数执行几个可能失败的操作。当它们失败时,它们会抛出一个错误,调用 JavaScript 代码可以捕捉到这个错误。因为构造函数执行的第一个操作是分配内存,所以如果发生异常,它需要释放内存。如果不这样做,内存就会被孤立,导致内存泄漏,最终导致系统故障。为了防止这种情况,构造函数用xsTry包围这些操作,用xsCatch捕捉任何异常。捕捉到异常后,构造函数释放存储在rn中的内存,然后使用xsThrow再次抛出错误。在 C 语言中,xsTryxsCatch使用结构如清单 11-17 所示。

xsTry {
    ...
}
xsCatch {
    free(rn);
    xsThrow(xsException);
}

Listing 11-17.

回想一下,C 中的 XS 提供了在 C 代码中访问和实现基本 JavaScript 功能的方法。xsTry - xsCatch的 C 代码类似于 JavaScript 版本的代码,如清单 11-18 所示。

try {
    ...
}
catch(e) {
    ...
    throw e;
}

Listing 11-18.

xsTry块首先声明一个局部变量poll,以保存字典参数请求的轮询间隔,并使用xsmcVars为 JavaScript 栈上的临时值保留空间:

int poll;

xsmcVars(1);

如清单 11-19 所示,构造函数然后调用xsmcHas来查看字典参数是否包含poll属性。如果是,则检索属性,转换成整数,并赋给局部变量poll;否则,将使用默认值 5,000。

if (xsmcHas(xsArg(0), xsID_poll)) {
    xsmcGet(xsVar(0), xsArg(0), xsID_poll);
    poll = xsmcToInteger(xsVar(0));
}
else
    poll = 5000;

Listing 11-19.

xsmcHas函数类似于 JavaScript 中使用的in操作符。前面的代码与清单 11-20 中的 JavaScript 代码大致相同。

let poll;
if ("poll" in options)
    poll = options.poll;
else
    poll = 5000;

Listing 11-20.

构造函数接下来再次调用xsmcHas,这次是为了确认所需的threshold属性是否存在。如果没有,它抛出一个错误;否则,将检索 JavaScript 的threshold属性,将其转换为整数,并分配给rnthreshold字段。

if (!xsmcHas(xsArg(0), xsID_threshold))
    xsUnknownError("threshold required");
xsmcGet(xsVar(0), xsArg(0), xsID_threshold);
rn->threshold = xsmcToInteger(xsVar(0));

最后,xsTry模块使用来自可修改 SDK 的modTimerAdd分配一个本地定时器。您可以在这里使用另一个定时器机制,一个特定于您的微控制器的机制。为了方便起见,这段代码使用了modTimerAdd,因为它可用于 ESP32 和 ESP8266 设备。如果计时器不能被分配——例如,因为没有足够的可用内存——构造函数抛出一个异常。

rn->timer = modTimerAdd(1, poll, checkRSSI, &rn, sizeof(rn));
if (!rn->timer)
    xsUnknownError("no timer");

modTimerAdd的调用创建了一个定时器,它首先在 1 毫秒后触发,然后在poll指定的时间间隔触发。当定时器触发时,它调用checkRSSI本地函数,传递给它rn的值。后面一节将展示本机回调如何检索这个值并调用 JavaScript 回调。

这就是xsTry块的结尾。即使在这个相对简单的对象中,也有两个异常是由构造函数本身生成的。此外,当传递一个不能转换成整数的值时,对xsmcToInteger的调用会抛出异常。这些潜在的异常使得构造函数确保在抛出异常时没有内存或其他资源成为孤儿变得非常重要。使用xsTryxsCatch通常有助于解决这个问题。

构造函数中还剩下两个步骤。第一个是用对象存储rn数据指针:

xsmcSetHostData(xsThis, rn);

第二是确保只有在 JavaScript 代码对对象调用了close之后,对象才被垃圾收集。对于支持回调的 JavaScript 宿主对象来说,这种行为很常见。为此,构造函数用存储在RSSINotifyRecord中的对象调用xsRemember函数。

xsRemember(rn->obj);

你只能在你的代码分配的存储器中传递一个值。如果您用诸如xsThisxsArg(0)xsVar(1)或其他 XS 提供的值来调用xsRemember,它会悄悄地失败。如您所料,有一个相应的xsForget调用需要在close中调用。存储对象的内存,这里是rn->obj,必须持续到xsForget被调用,因此不能是构造函数中的局部变量。

析构函数

WiFiRSSINotify(清单 11-21 )的析构函数类似于本章中的其他析构函数,只是增加了代码来释放在构造函数中分配的计时器。为了访问RSSINotifyRecord结构中的定时器,数据指针参数被转换成一个RSSINotify指针。当rn为非NULL时,构造函数实现保证timer字段永远不会成为析构函数中的NULL。因此,在调用modTimerRemove之前,不需要检查rn->timer是否为非NULL

void xs_wifirssinotify_destructor(void *data)
{
    RSSINotify rn = data;
    if (rn) {
        modTimerRemove(rn->timer);
        free(rn);
    }
}

Listing 11-21.

close功能

WiFiRSSINotify(清单 11-22 )的close方法也遵循一种熟悉的模式。然而,除此之外,它必须调用xsForget来使对象符合垃圾收集的条件,抵消对构造函数中xsRemember的调用。因为对xsForget的调用访问rnobj字段,所以close实现必须通过检查xsmcGetHostData是否返回非NULL值来防止被调用多次。

void xs_wifirssinotify_close(xsMachine *the)
{
    RSSINotify rn = xsmcGetHostData(xsThis);
    if (rn) {
        xsForget(rn->obj);
        xs_wifirssinotify_destructor(rn);
        xsmcSetHostData(xsThis, NULL);
    }
}

Listing 11-22.

xsForget的调用不能在析构函数中进行,因为析构函数不能在 C 中使用 XS,如前所述。

回电

清单 11-23 中显示的checkRSSI函数是WiFiRSSINotify类的核心。它在轮询间隔被调用,以检测 RSSI 值何时超过指定的阈值。该函数从恢复rn的值开始,该值是指向在构造函数中分配的RSSINotifyRecord结构的指针。因为checkRSSI回调不是由 XS 直接调用的,而是由modTimer调用的,所以指针不能像往常一样使用xsmcGetHostData来检索,而是通过解引用refcon参数来检索。

void checkRSSI(modTimer timer, void *refcon, int refconSize)
{
    RSSINotify rn = *(RSSINotify *)refcon;
    ...
}

Listing 11-23.

下一步是获取当前 RSSI 值,这在 ESP32 和 ESP8266 上以不同方式完成。清单 11-24 对每个目标都有条件情况,对其他目标有一个错误。

int rssi = 0;

#if ESP32
    wifi_ap_record_t config;

    if (ESP_OK == esp_wifi_sta_get_ap_info(&config))
        rssi = config.rssi;
#elif defined(__ets__)
    rssi = wifi_station_get_rssi();
#else
    #error Unsupported target
#endif

Listing 11-24.

如清单 11-25 所示,轮询函数使用当前的 RSSI 值来决定是否需要调用onStrongSignalonWeakSignal JavaScript 回调函数。它检查当前值是高于还是低于存储在rn->threshold中的指定阈值。如果 RSSI 值与前一次检查在阈值的同一侧,checkRSSI立即返回;否则,它会将rn->state更新为新状态,并将要调用的回调的 IDxsID_onStrongSignalxsID_onWeakSignal赋给局部变量callbackID

if (rssi > rn->threshold) {
    if (kRSSIStrong == rn->state)
        return;
    rn->state = kRSSIStrong;
    callbackID = xsID_onStrongSignal;
}
else {
    if (kRSSIWeak == rn->state)
        return;
    rn->state = kRSSIWeak;
    callbackID = xsID_onWeakSignal;
}

Listing 11-25.

从本机代码调用 JavaScript 函数需要有效的 JavaScript 栈框架。当 JavaScript 调用本地方法时,XS 已经创建了栈框架。checkRSSI函数不是由 XS 调用的,而是由modTimer调用的,因此必须自己设置栈框架。它通过在回调之前调用xsBeginHost来做到这一点。它随后调用xsEndHost来移除xsBeginHost创建的栈帧。这两个函数都将引用 JavaScript 虚拟机的the作为唯一的参数。在xsBeginHostxsEndHost之间,你可以照常用 C 打电话到 XS。

清单 11-26 中的代码使用xsmcVars(1)创建一个临时的 JavaScript 变量,并使用xsmcSetInteger为其分配一个整数值rssi。然后它调用xsmcHas来确认对象有回调函数。如果是,它使用xsCall调用回调函数,传递存储在xsVar(0)中的 RSSI 值。

xsBeginHost(rn->the);
    xsmcVars(1);
    xsmcSetInteger(xsVar(0), rssi);
    if (xsmcHas(rn->obj, callbackID))
        xsCall1(rn->obj, callbackID, xsVar(0));
xsEndHost(rn->the);

Listing 11-26.

使用xsCall1调用带有一个参数的函数(使用xsCall0调用不带参数的函数,使用xsCall2调用带有两个参数的函数,依此类推,直到xsCall9)。

其他技术

现在,您已经知道如何从 JavaScript 代码中调用本机代码,以及如何从本机代码中调用 JavaScript 代码,这使您能够以最适合您的项目的方式集成本机代码和脚本。本节简要介绍了几个重要的主题,在将本机代码集成到您自己的 JavaScript 支持的产品中时,您可能会发现这些主题很有用。除了讨论帮助您在本机代码和 JavaScript 代码之间搭建桥梁的各种技术之外,它还包括一些常见错误的警告。

调试本机代码

随着您开发越来越复杂的本机代码,您可能需要调试这些代码。尽管您可能没有可用的本机调试器,但您的代码可以与xsbug交互。

一种常见的调试技术是将诊断输出发送到调试控制台。在嵌入式 JavaScript 中,您使用trace来做这件事。在 C 中使用 XS,你可以用xsTrace做同样的事情。

xsTrace("about to get RSSI\n");

xsTrace的参数是一个字符串,便于输出函数的进度。如果您需要输出更详细的信息,使用xsLog,它提供了printf风格的功能。

xsLog("RSSI is %d.\n", rssi);

xsTracexsLog都需要有效的 XS 栈帧;因此,它们必须要么从 XS 直接调用的方法中调用,要么在xsBeginHost - xsEndHost对之间调用。例如,要从checkRSSI回调中将当前的 RSSI 级别输出到调试控制台,可以使用以下代码:

xsBeginHost(rn->the);
    xsLog("RSSI is %d.\n", rssi);
xsEndHost(rn->the);

在本机代码的xsbug中触发一个断点会很有用,这样可以查看导致调用本机函数的栈帧以及传递给它的参数。虽然不能使用xsbug在本机代码中设置断点,但是可以通过在 C 代码中调用xsDebugger来触发断点。

xsDebugger();

访问全局变量

您的代码可以直接获取和设置全局变量的值。所有的全局变量都是全局对象的一部分,在 JavaScript 中使用globalThis来访问全局对象。在 C 语言的 XS 中,全局对象作为xsGlobal对本地代码可用。您可以像使用任何其他对象一样在本机代码中使用xsGlobal。例如,您使用xsmcSet*函数为一个全局变量赋值,下面几行代码将全局变量status设置为0x8012:

xsmcSetInteger(xsVar(0), 0x8012);
xsmcSet(xsGlobal, xsID_status, xsVar(0));

使用xsmcGet可以得到一个全局的值:

xsmcGet(xsVar(0), xsGlobal, xsID_status);
int status = xsmcToInteger(xsVar(0));

下面的代码检查是否有一个名为onRestart的全局变量。如果有,它调用存储在onRestart全局中的函数。

if (xsmcHas(xsGlobal, xsID_onRestart))
    xsCall0(xsGlobal, xsID_onRestart);

获取函数的返回值

当您使用xsCall*函数族从 C 调用 JavaScript 函数时,您可以通过将结果赋给一个 JavaScript 值来访问返回值。例如,下面的代码调用thiscallback属性上的函数,并将结果跟踪到控制台:

xsmcVars(1);
xsVar(0) = xsCall0(xsThis, xsID_callback);
xsTrace(xsVar(0));

获取值

本章中的例子使用xsmcToInteger从 JavaScript 值中获取一个整数值。从 JavaScript 值中获取布尔值、浮点数、字符串和ArrayBuffer也有类似的函数,如清单 11-27 所示。

uint8_t boolean = xsmcToBoolean(xsArg(0));
double number = xsmcToNumber(xsArg(1));
const char *str = xsmcToString(xsArg(2));
uint8_t *buffer = xsmcToArrayBuffer(xsArg(3));
int bufferLength = xsmcGetArrayBufferLength(xsArg(3));

Listing 11-27.

如果 JavaScript 值不能转换成请求的类型,所有这些函数都会失败。例如,如果值是字符串,xsmcToArrayBuffer将失败。

使用指向字符串的指针和ArrayBuffer指针时需要特别小心。有关详细信息,请参见“确保缓冲区指针有效”一节。

设置值

您已经看到了如何使用xsmcSetInteger将 JavaScript 属性设置为整数值。此外,还有用于设置其他基本 JavaScript 值的xsmcSet*函数,如清单 11-28 所示。

xsmcSetNull(xsResult);
xsmcSetUndefined(xsVar(0));

xsmcSetBoolean(xsVar(2), value);
xsmcSetTrue(xsVar(3));
xsmcSetFalse(xsResult);

xsmcSetNumber(xsResult, 1.2);

xsmcSetString(xsResult, "off");

const char *string = "a dog!";
xsmcSetStringBuffer(xsResult, string + 2, 3); // "dog"

Listing 11-28.

您也可以在 c 中使用 XS 创建对象。下面的代码创建一个 16 字节的ArrayBuffer对象,并将第一个字节设置为 1:

xsmcSetArrayBuffer(xsResult, NULL, 16);
uint8_t *buffer = xsmcToArrayBuffer(xsResult);
buffer[0] = 1;

清单 11-29 创建一个对象并给它添加几个属性。使用这种方法,您的代码可以像File类的next方法一样返回对象。

xsmcSetNewObject(xsResult);

xsmcSetString(xsVar(0), "test.txt");
xsmcSet(xsResult, xsID_name, xsVar(0));

xsmcSetInteger(xsVar(0), 1024);
xsmcSet(xsResult, xsID_length, xsVar(0));

Listing 11-29.

该代码的 JavaScript 等效代码如下:

return {name: "test.txt", length: 1024};

清单 11-30 创建一个包含八个元素的数组,并使用xsmcSet将每个数组元素设置为其索引的平方。你已经看到了xsmcSet用于设置一个对象的属性值;这里它用来通过传递元素的索引而不是一个xsID_*风格的符号标识符来设置数组元素的值。

xsmcSetNewArray(xsResult, 8);

for (i = 0; i < 8; i++) {
    xsmcSetInteger(xsVar(0), i * i);
    xsmcSet(xsResult, i, xsVar(0));
}

Listing 11-30.

确定值的类型

您的本机代码有时需要知道 JavaScript 值的类型。例如,一些函数根据参数是对象还是数字来改变它们的行为。您使用xsmcTypeOf来确定一个值的基本类型。

int typeOf = xsmcTypeOf(xsArg(1));
if (xsStringType == typeOf)
    ...;

xsmcTypeOf返回的类型有xsUndefinedTypexsNullTypexsBooleanTypexsIntegerTypexsNumberTypexsStringTypexsReferenceType。其中大多数都直接对应于您已经熟悉的 JavaScript 类型。但是,请注意,整数和数字(浮点值)都有类型。虽然 JavaScript 本身对两者都使用了Number类型,但作为一种优化,XS 将它们存储为不同的类型。如果您的本地代码检查一个 JavaScript 值是否属于类型Number,它需要检查xsIntegerTypexsNumberType

类型xsReferenceType对应于一个 JavaScript 对象。这个单一类型的常量用于所有 JavaScript 对象。您使用xsmcIsInstanceOf函数来确定对象是否是特定类的实例。类型xsmcIsInstanceOf类似于 JavaScript 的instanceof操作符。XS 为内置对象定义值,例如,xsArrayPrototype。如果本地方法的第一个参数是数组,下面的代码将变量isArray设置为 1,否则设置为 0:

int typeOf = xsmcTypeOf(xsArg(0));
int isArray = (xsReferenceType == typeOf) &&
              xsmcIsInstanceOf(xsArg(0), xsArrayPrototype);

如果对象是指定类型的子类,xsmcIsInstanceOf函数返回true。例如,第 2 章中的“访问数据视图的值”一节将Header类定义为DataView的子类。将Header的实例传递给下面的调用会返回true:

if (xsmcIsInstanceOf(xsArg(0), xsDataViewPrototype))
    ...;    // is a data view

XS 定义的其他可用的原型包括xsFunctionPrototypexsDatePrototypexsErrorPrototypexsTypedArrayPrototype。有关完整列表,请参见可修改 SDK 中的xs.h头文件。

使用字符串

JavaScript 中经常使用字符串。因为 XS 以 UTF-8 编码存储字符串,所以在 c 语言中处理字符串很方便。以下是一些需要记住的细节:

  • 你保证你从 XS 收到的字符串是有效的 UTF-8。您必须确保传递给 XS 的任何字符串也是有效的 UTF-8。

  • XS 将空字符(ASCII 0)视为字符串的结尾,所以不要在字符串中包含任何空字符。(因为 C 语言也使用空字符来结束字符串,所以这应该很熟悉。)您的代码可能不会故意创建无效的 UTF-8 字符串或在字符串中包含空字符,但当您从文件或网络连接中导入字符串时,它们可能会溜进来;在将这些字符串传递给 XS 之前,验证它们是一个很好的做法。

  • 在 JavaScript 中,字符串是只读的。没有提供改变字符串内容的函数。您可以选择在本机代码中打破这条规则,但是不要这样做!这样做会打破 JavaScript 程序员所依赖的一个基本假设。此外,它可能会导致崩溃,因为一些字符串存储在只读闪存中,试图写入这些字符串会导致微控制器复位。

  • 当您在 c 中使用 XS 进行其他调用时,从xsmcToString返回的字符串指针可能会失效。下一节将解释细节。

确保缓冲区指针有效

当你调用xsmcToStringxsmcToArrayBuffer时,它们不返回数据的副本;它们返回一个指向 XS 数据结构的指针。这种行为在微控制器上很重要,在微控制器上制作一份拷贝所需的额外时间和内存是不可接受的。当您在 C 中调用 XS 导致垃圾收集器运行时,该指针可能会变得无效。垃圾收集器无法释放ArrayBuffer或字符串,因为它们正在被使用。但是,垃圾收集器在压缩内存堆时可能会移动数据结构,通过合并空闲空间区域来释放更多空间。

如以下方法所示,只要小心,就可以避免垃圾收集器压缩堆时出现任何问题:

  • 永远不要在 C 中再次调用 XS 后使用 XS 返回的指针,这看起来很有挑战性,但是本章中所有的例子都是这样做的。

  • 复制一份数据。虽然这种方法不是最佳的,但偶尔也是必要的。

当你使用指向字符串的指针和ArrayBuffer指针时,有两个函数会有所帮助。xsmcToStringBuffer函数类似于xsmcToString,但是它不是返回一个字符串指针,而是将字符串复制到一个缓冲区。如果缓冲区太小,无法容纳字符串,就会抛出一个RangeError错误。

char str[40];
xsmcToStringBuffer(xsArg(0), str, sizeof(str));

xsmcGetArrayBufferData函数将全部或部分ArrayBuffer复制到另一个缓冲区。第二个参数是开始复制数据的ArrayBuffer偏移量(以字节为单位),第三个参数是目标缓冲区,最后一个参数是目标缓冲区的大小(以字节为单位)。这个例子将从偏移量 10 开始的五个字节从一个ArrayBuffer复制到本地变量buffer

uint8_t buffer[5];
xsmcGetArrayBufferData(xsResult, 10, buffer, sizeof(buffer));

与 C++集成

C 语言中的 XS 不仅能让你在 C 和 JavaScript 代码之间架起一座桥梁,还能让你在 C++和 JavaScript 代码之间架起一座桥梁。虽然 JavaScript 和 C++都支持对象,但是它们实现对象的细节和特性却大不相同。因此,试图在 C++类和 JavaScript 类之间建立直接映射通常是不现实的。相反,设计 JavaScript 类时要让 JavaScript 程序员明白,设计 C++类时要让 C++程序员明白。你用 C 语言中的 XS 编写的桥代码可以在两者之间进行转换。

使用线程

JavaScript 是单线程语言;因此,XS JavaScript 引擎也是单线程的。这意味着对单个 JavaScript 虚拟机的所有调用,如由the表示的对本地代码的调用,应该由同一个线程或任务进行。在 C 语言中,你不应该从一个中断或线程中调用 XS,而不是从创建虚拟机的线程中调用。

提供 JavaScript 代码多任务执行的技术,比如 Web Workers 类,是在 JavaScript 语言之外构建的。可修改的 SDK 支持 ESP32 上 Web Workers 类的子集,这使得几个 JavaScript 虚拟机可以共存,每个虚拟机都有自己的线程。每个虚拟机都是单线程的,但是几个机器可以并行运行。ESP32 的 Web Workers 实现考虑了每个单独的 JavaScript 虚拟机都是单线程的要求。

结论

使用 C API 中的 XS 在 JavaScript 和本机代码之间架起桥梁的能力为您的项目打开了许多新的可能性之门。它使您能够优化内存使用、提高性能、重用现有的 C 和 C++代码库,以及访问独特的硬件功能。然而,在 C 中使用 XS 比在 JavaScript 中要困难得多,因此也更容易出错。通常,尽可能少地使用本机代码有助于最小化风险。

为了帮助您了解更多关于在 C #中使用 XS 的知识,可以使用以下两个优秀的资源:

  • C #中的 XS 文档是对该 API 的完整参考。它是可修改 SDK 的一部分。

  • 可修改的 SDK 中访问本机功能的所有类都是使用 c 中的 XS 实现的。如果您对它们的工作方式感到好奇,源代码可供您阅读和学习。