十一、添加本机代码
有时候,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_random
。esp_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 位有符号值。因此,使用硬件随机数技术会更改 JavaScriptrandomInt
函数的结果,以返回从–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 代码中熟悉的try
和catch
块捕捉到。
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 类型化数组,比如Uint8Array
和Uint32Array
,使您能够使用最少的内存处理 8 位、16 位和 32 位整数值的数组。BitArray
类实现了一个 1 位数组——也就是说,一个只存储值 0 和 1 的数组。这对于有效存储从数字输入接收的大量样本非常有用。
本节介绍了BitArray
的两种变体,每一种都有相同的 JavaScript API。第一个使用 JavaScript ArrayBuffer
来存储位,而第二个使用 C calloc
函数分配的本地内存。
BitArray
类构造函数接受一个参数:数组需要存储的位数。该类提供了get
和set
方法来访问数组中的值。清单 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.
get
和set
的第一个参数是数组中要获取或设置的位的索引。第一个数组元素的索引为 0。该示例的最后一行切换索引 3 处的位的值。
使用由ArrayBuffer
分配的内存
清单 11-6 显示了$EXAMPLES/ch11-native/bitarray-arraybuffer
示例中BitArray
的实现。它从用 JavaScript 声明类开始。与前面的随机整数示例一样,特殊的 XS @
语法用于将 JavaScript 函数连接到本地 C 函数。注意,构造函数是用 JavaScript 实现的,而get
和set
方法是用 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
属性。get
和set
的本地实现使用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 变量,通过调用带有参数1
的xsmcVars
来指定临时变量的数量。
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 语言中表达起来要冗长得多。与调用xsmcVars
和xsmcGet
等效的 JavaScript 如下:
let var0;
var0 = this.buffer;
buffer
属性不是指向ArrayBuffer
实例使用的内存缓冲区的指针;这是对实例的引用。正如使用xsmcToInteger
将 JavaScript 值转换成整数一样,使用xsmcToArrayBuffer
将 JavaScript 值转换成本机指针。如果 JavaScript 值不是一个ArrayBuffer
实例,那么对xsmcToArrayBuffer
的调用会抛出一个异常。
uint8_t *buffer = xsmcToArrayBuffer(xsVar(0));
现在xs_bitarray_get
有了缓冲区指针,它使用之前计算的byteIndex
和bitIndex
值来读取该位,并将 JavaScript 函数调用的返回值设置为 0 或 1。
if (buffer[byteIndex] & (1 << bitIndex))
xsmcSetInteger(xsResult, 1);
else
xsmcSetInteger(xsResult, 0);
set
功能
xs_bitarray_set
中set
函数(清单 11-7 的实现与get
的实现非常相似。以同样的方式确定byteIndex
、bitIndex
和buffer
的值。唯一的区别是第二个参数的值(通过xsArg(1)
访问)用于确定是否设置或清除指定的位。
int value = xsmcToInteger(xsArg(1));
if (value)
buffer[byteIndex] |= 1 << bitIndex;
else
buffer[byteIndex] &= ~(1 << bitIndex);
Listing 11-7.
安全漏洞
使用由ArrayBuffer
分配的内存的BitArray
的实现工作得很好,但是它有一个致命的缺陷,使它不适合在实际产品中安全使用。get
和set
函数不验证index
参数是否在分配的内存范围内。这使得使用此BitArray
实现的代码能够读写嵌入式设备上的任意内存,这可能导致崩溃或被用作隐私攻击的基础。有多种方法可以解决这个问题;下一节将讨论其中之一。
使用由calloc
分配的内存
在$EXAMPLES/ch11-native/bitarray-calloc
示例中BitArray
的实现解决了刚才讨论的bitarray-arraybuffer
示例所带来的安全问题。它存储构造函数分配的位数,然后根据存储的值验证传递给get
和set
调用的索引。
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.
get
和set
方法的声明与前面的例子相同,尽管实现有些不同。
本机构造函数、析构函数和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
调用通过为代码提供一种显式释放这些资源的方式来解决这个问题。
许多主机对象都有一个类似于BitArray
的close
实现(清单 11-11 )。
void xs_bitarray_close(xsMachine *the)
{
uint8_t *buffer = xsmcGetHostData(xsThis);
xs_bitarray_destructor(buffer);
xsmcSetHostData(xsThis, NULL);
}
Listing 11-11.
下面是这几行代码的作用:
-
对
xsmcGetHostData
的调用检索在构造函数中分配的数据指针,并通过对xsmcSetHostData
的调用与该对象相关联。 -
数据指针被传递给析构函数,析构函数负责释放资源。
-
对
xsmcSetHostData
的调用将保存的数据指针设置为NULL
。这确保了如果close
被调用两次,数据指针只被释放一次。
get
和set
功能
xs_bitarray_get
的实现计算位和字节索引值的方式与get
的ArrayBuffer
版本相同:
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
来检索在构造函数中创建的数据指针。如果实例已经关闭——也就是说,如果buffer
是NULL
——它抛出一个异常;否则,它将返回值设置为从数据指针开始处提取的位计数。
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";
}
用于onWeakSignal
和onStrongSignal
回调的默认函数不是该类的一部分。在调用回调之前,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
,然后是kRSSIWeak
或kRSSIStrong
。该状态用于在状态没有改变时消除多余的回调。 -
用于实现轮询的本地定时器。
-
the –
对包含WiFiRSSINotify
实例的 XS 虚拟机的引用。它用于从计时器中调用回调。 -
obj –
对用于从计时器调用回调的WiFiRSSINotify
对象的引用。这个字段的类型xsSlot
,被 XS 用来保存任何 JavaScript 值。您已经知道的xsArg
、xsVar
和xsGet
函数返回类型xsSlot
的值。
以下部分提供了有关如何使用这些字段的更多详细信息。
为了方便起见,该实现还将RSSINotify
定义为指向RSSINotifyRecord
的指针:
typedef struct RSSINotifyRecord *RSSINotify;
构造函数
WiFiRSSINotify
构造函数首先为RSSINotifyRecord
结构分配存储空间。一旦这个结构被完全初始化,它就被使用xsmcSetHostData
附加到对象上。通常,数据结构在初始化之前不会附加到对象上,以避免在构造函数执行期间发生错误时拥有部分初始化的结构。
RSSINotify rn = calloc(sizeof(RSSINotifyRecord), 1);
if (!rn)
xsUnknownError("no memory");
接下来,构造函数初始化state
、the
和obj
字段:
rn->state = kRSSIUnknown;
rn->obj = xsThis;
rn->the = the;
构造函数执行几个可能失败的操作。当它们失败时,它们会抛出一个错误,调用 JavaScript 代码可以捕捉到这个错误。因为构造函数执行的第一个操作是分配内存,所以如果发生异常,它需要释放内存。如果不这样做,内存就会被孤立,导致内存泄漏,最终导致系统故障。为了防止这种情况,构造函数用xsTry
包围这些操作,用xsCatch
捕捉任何异常。捕捉到异常后,构造函数释放存储在rn
中的内存,然后使用xsThrow
再次抛出错误。在 C 语言中,xsTry
和xsCatch
使用结构如清单 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
属性,将其转换为整数,并分配给rn
的threshold
字段。
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
的调用会抛出异常。这些潜在的异常使得构造函数确保在抛出异常时没有内存或其他资源成为孤儿变得非常重要。使用xsTry
和xsCatch
通常有助于解决这个问题。
构造函数中还剩下两个步骤。第一个是用对象存储rn
数据指针:
xsmcSetHostData(xsThis, rn);
第二是确保只有在 JavaScript 代码对对象调用了close
之后,对象才被垃圾收集。对于支持回调的 JavaScript 宿主对象来说,这种行为很常见。为此,构造函数用存储在RSSINotifyRecord
中的对象调用xsRemember
函数。
xsRemember(rn->obj);
你只能在你的代码分配的存储器中传递一个值。如果您用诸如xsThis
、xsArg(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
的调用访问rn
的obj
字段,所以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 值来决定是否需要调用onStrongSignal
或onWeakSignal
JavaScript 回调函数。它检查当前值是高于还是低于存储在rn->threshold
中的指定阈值。如果 RSSI 值与前一次检查在阈值的同一侧,checkRSSI
立即返回;否则,它会将rn->state
更新为新状态,并将要调用的回调的 IDxsID_onStrongSignal
或xsID_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
作为唯一的参数。在xsBeginHost
和xsEndHost
之间,你可以照常用 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);
xsTrace
和xsLog
都需要有效的 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 值来访问返回值。例如,下面的代码调用this
的callback
属性上的函数,并将结果跟踪到控制台:
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
返回的类型有xsUndefinedType
、xsNullType
、xsBooleanType
、xsIntegerType
、xsNumberType
、xsStringType
、xsReferenceType
。其中大多数都直接对应于您已经熟悉的 JavaScript 类型。但是,请注意,整数和数字(浮点值)都有类型。虽然 JavaScript 本身对两者都使用了Number
类型,但作为一种优化,XS 将它们存储为不同的类型。如果您的本地代码检查一个 JavaScript 值是否属于类型Number
,它需要检查xsIntegerType
和xsNumberType
。
类型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 定义的其他可用的原型包括xsFunctionPrototype
、xsDatePrototype
、xsErrorPrototype
和xsTypedArrayPrototype
。有关完整列表,请参见可修改 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
返回的字符串指针可能会失效。下一节将解释细节。
确保缓冲区指针有效
当你调用xsmcToString
或xsmcToArrayBuffer
时,它们不返回数据的副本;它们返回一个指向 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 实现的。如果您对它们的工作方式感到好奇,源代码可供您阅读和学习。
版权属于:月萌API www.moonapi.com,转载请注明出处