© Peter Hoddie and Lizzie Prader 2020
P. Hoddie, L. PraderIoT Development for ESP32 and ESP8266 with JavaScripthttps://doi.org/10.1007/978-1-4842-5070-9_11

11. Adding Native Code

Peter Hoddie1  and Lizzie Prader1
(1)
Menlo Park, CA, USA
 

There are times when JavaScript isn’t the best language to use to implement parts of your IoT product. Fortunately, you don’t need to choose either JavaScript or C (or C++) to build your product: you can choose both. XS in C is a low-level C API provided by the XS JavaScript engine so that you can integrate C code into your JavaScript projects (or JavaScript code into your C projects!).

Here are three common reasons for using use native code in your project:
  • Performance – High-level languages, including JavaScript, can’t outperform optimized native code at high-performance tasks. You can add your own optimized native functions and invoke them from your JavaScript code.

  • Accessing hardware features – As a general-purpose programming language, JavaScript doesn’t have built-in support for the unique features of your host hardware. You can implement your own functions and classes to configure and use these.

  • Reusing existing native code – You may have a large body of existing native code that works well for your products, and you’d prefer not to have to rewrite it in JavaScript. You can use that code in your JavaScript projects by using XS in C to bridge between it and your JavaScript code.

XS in C lets you work with JavaScript features from C. As you know, JavaScript has capabilities that C doesn’t directly support, such as dynamic types and objects. Working with these features using XS in C can be awkward, but it becomes straightforward as you get some practice and learn some common patterns. This chapter introduces XS in C through a series of examples that demonstrate different techniques to build a bridge between JavaScript and C code.

Note that many engines that implement a high-level programming language provide an API to bridge between that language and native code. The Java language defines the Java Native Interface (JNI) for this purpose, and the V8 JavaScript engine provides a C++ API.

Important

The information introduced in this chapter is an advanced topic. It assumes you’re comfortable programming in C and have a solid understanding of the basic JavaScript concepts discussed in this book.

Installing the Host

There’s no host to install for this chapter, because all native code must be part of the host itself; therefore, you build each example in this chapter as a standalone host. Rather than using mcrun to install the examples, you use mcconfig. The following command lines are for ESP32 and ESP8266 targets, respectively:
> mcconfig -d -m -p esp32
> mcconfig -d -m -p esp

These command lines don’t specify a development board (for example, esp32/moddable_two) because the examples use only common features of the microcontroller and don’t depend on board-specific features.

When you build the examples with mcconfig, both the JavaScript and the C code are built. If an error occurs building either, it’s reported to the command line.

Generating Random Integers

The first example of native code integration generates random integers. You saw in Chapter 9 that the random-rectangles example uses random numbers generated by the JavaScript built-in function Math.random. That example is less efficient than it could be because Math.random returns a floating-point value, forcing Poco to convert several floating-point values to integers for each rectangle. Floating-point operations are generally slow on microcontrollers, and here they have no benefit. The C standard library’s rand function generates random integers, and the $EXAMPLES/ch11-native/random-integer example begins by using rand to generate random integers for JavaScript code.

Creating a Native Function

The first step is to create a JavaScript function that JavaScript code can call to invoke your C function. The random-integer example declares a randomInt function in the main.js source code file.
function randomInt() @ "xs_randomInt";

This syntax creates a JavaScript function named randomInt which, when called, invokes the native function xs_randomInt, essentially building a bridge from JavaScript to C. The use of @ here is not standard JavaScript syntax but a language extension provided by XS to simplify adding native code to your projects. Consequently, this code is unlikely to compile or work the same with other JavaScript engines.

After creating the function, you can call it like any other JavaScript function. The main.js module calls it 100 times, tracing the result to the debug console.
for (let i = 0; i < 100; i++)
    trace(randomInt(), " ");

Implementing a Native Function

The implementation of xs_randomInt is contained in main.c. When you build a file with a .js extension, mcconfig also builds a file with a .c extension that has the same name. Listing 11-1 shows the entire contents of main.c.
#include "xsmc.h"
void xs_randomInt(xsMachine *the)
{
    xsmcSetInteger(xsResult, rand());
}

Listing 11-1.

The include preprocessor command brings in the header file for XS in C. (The file name, xsmc, stands for “XS Microcontroller.”) There’s also an xs.h header file that’s used by some code. The two headers provide equivalent functionality, but the functions in the xsmc.h header file are more efficient and therefore preferred for use on microcontrollers.

The native function prototype of xs_randomInt is used for all functions that implement native methods using XS in C. The JavaScript arguments are not passed as arguments to the C function. You’ll see later in this chapter how to access the arguments.

This example needs to return a value—the result of calling rand. The result of rand is an integer, so this example uses xsmcSetInteger, a function that assigns a native 32-bit integer value to a JavaScript value. Here the JavaScript value is xsResult, which refers to the return value of the function on the JavaScript stack.

Using the Hardware Random Number Generator

You’ve seen how simple it is to declare, call, and implement a simple native function. When you run the random-integer example, you see 100 random numbers from 0 to 2,147,483,647 traced to the debug console. But when you restart the microcontroller and run the example a second time, you see the exact same list of numbers. That’s not very random. Why does it happen?

The rand function is a pseudo-random number generator. It’s an algorithm to generate numbers that appear random; however, when you restart the microcontroller you also restart the pseudo-random number generator algorithm, causing it to generate the same sequence of numbers. You can use the srand function to have the algorithm start a different sequence, but you must provide srand with a different starting point on each restart. The most common way to initialize the sequence is to use the current time. Unfortunately, many microcontrollers, including the ESP32 and ESP8266, don’t know the time at startup, so this technique can’t be applied.

Fortunately, many microcontrollers, including the ESP32 and ESP8266, have hardware to generate random numbers, and these values are more random than those generated by rand. The $EXAMPLES/ch11-native/random-integer-esp example shows how to use the hardware random number generator.

Important

Not all random numbers are guaranteed to be sufficiently unpredictable to be safely used in security solutions, such as the TLS protocol that protects network connections. (Random numbers that have this guarantee are called cryptographically secure.) You should always verify that the source of random numbers you use meets the security requirements of your project. This isn’t easy to do, but it’s important, as a weak random number generator is a vulnerability in your project’s overall security.

On the ESP32, accessing the hardware random number generator requires just substituting the call to rand with a call to the ESP-IDF function esp_random. The degree of randomness that esp_random provides depends on a number of factors, including whether the radio (Wi-Fi or Bluetooth) is enabled.
xsmcSetInteger(xsResult, esp_random());
On the ESP8266, there’s an undocumented hardware random number generator that appears to work well. It should be used with care, as its precise characteristics are not known. To access the random number generator, you read its hardware register directly.
uint32_t random = *(volatile uint32_t *)0x3FF20E44;
Listing 11-2 shows the revised native implementation using the native random number generators. Because the generator is accessed differently on the ESP32 and the ESP8266, the C code uses conditional compilation to select the correct version and to generate an error when the code is compiled for an unsupported target.
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.

There are two problems with using this randomInt function :
  • Both the ESP32 and ESP8266 hardware random number generators return 32-bit unsigned values. The xsmcSetInteger function requires a 32-bit signed value. Consequently, using the hardware random number technique changes the result of the JavaScript randomInt function to return a range of values from –2,147,483,648 to 2,147,483,647. Recall that when you use rand, all values are positive. You could use xsmcSetNumber instead to return the unsigned 32-bit value as a floating-point number; however, that runs counter to the goal of returning a random number as an integer value.

  • Usually you want a random number within a certain range, and generating a value within a range requires a division or modulo operation. The division operation typically requires a floating-point operation, since the result may have a fractional part. The modulo operation can use an integer divide if both operands are integers. However, instead of requiring the caller of randomInt to efficiently restrict the return value to the desired range, you can modify the native function to do that.

The next section addresses these issues.

Restricting Random Numbers to a Range

The $EXAMPLES/ch11-native/random-integer-esp-range example restricts random numbers to a range. The first step is to declare a function that accepts a range for the random values. The randomIntRange function accepts a single argument indicating the range of random values, starting at 0 and ending at max.
function randomIntRange(max) @ "xs_randomIntRange";
The calling code in main.js is updated to pass in the range, which is 1,000 in this example.
for (let i = 0; i < 100; i++)
    trace(randomIntRange(1000), " ");

The native function must first retrieve the range passed as the first argument. The arguments are accessed by index using xsArg. Arguments are numbered starting at 0, so the first argument is accessed as xsArg(0). If the caller didn’t pass any arguments, xsArg(0) throws an exception; therefore, it’s not usually necessary for your native code to check the number of arguments passed. (If your function needs to know the number of arguments, use the xsmcArgc integer value.) The exceptions thrown by XS in C are ordinary JavaScript exceptions, which may be caught with a familiar try and catch blocks in the JavaScript code.

The C code can’t make any assumption about the type of the argument, because JavaScript doesn’t enforce any rules about the types of arguments passed to a function. XS in C provides functions to convert a JavaScript value to a specific native type. In the xs_randomIntRange function (Listing 11-3), the call to xsmcToInteger asks XS to convert the JavaScript property to a signed 32-bit integer. If XS is able to perform the conversion, it returns the result; otherwise, it throws a JavaScript exception. For example, passing a string value of "100" or a number value of 100.1 succeeds because JavaScript knows how to convert them to an integer; however, passing an empty object {} fails.
void xs_randomIntRange(xsMachine *the)
{
    int range = xsmcToInteger(xsArg(0));
    if (range < 2)
        xsRangeError("invalid range");
    ...
}

Listing 11-3.

The native function implementation next validates the requested range. A range smaller than two values makes no sense for integer random numbers. If the range is invalid, the function calls xsRangeError to throw the JavaScript error RangeError. The preceding C code is equivalent to these lines of JavaScript:
if (range < 2)
    throw new RangeError("invalid range");

It’s important to include error checking in the native code that bridges between your JavaScript and C code. JavaScript programmers expect the language to be safe—there should be no way to crash or corrupt the device—and the JavaScript engine and runtime do their best to achieve this goal. Your native code must do the same. For example, should the JavaScript code pass 0 for the range, the result is undefined by the C language. The modulo operation with a 0 on the right side on ESP32 generates an IntegerDivideByZero exception and on ESP8266 an Illegal Instruction exception, both of which reset the microcontroller.

The remaining implementation of xs_randomIntRange (Listing 11-4) is straightforward. Instead of returning the 32-bit unsigned integer value directly, the modulo operator (%) restricts the random value to the specified range.
#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.

Comparing Random Number Approaches

The native randomIntRange function is just a few lines of native code, but those few lines have many advantages for IoT development compared to the built-in Math.random function :
  • The returned values are integers, not floating-point, allowing for more efficient execution on microcontrollers.

  • The returned values are efficiently limited to a requested range.

  • The numbers are more random because they use a hardware random number generator.

Of course, there are also disadvantages:
  • The native code isn’t portable. It builds successfully for only two microcontrollers.

  • You must build your native code as part of a host.

  • Native code is more complex to implement and debug and requires additional specialized knowledge.

When you have the option of adding native functionality to your project, you should base your decision on a balance of the advantages and the disadvantages.

The BitArray Class

JavaScript typed arrays, such as Uint8Array and Uint32Array, enable you to work with arrays of 8-, 16-, and 32-bit integer values using a minimum of memory. The BitArray class implements a 1-bit array—that is, an array that stores only the values 0 and 1. This is useful for efficiently storing a large number of samples received from a digital input.

This section introduces two variations of BitArray, each with the same JavaScript API. The first one uses a JavaScript ArrayBuffer to store the bits, while the second uses native memory allocated with the C calloc function.

The BitArray class constructor takes a single argument: the number of bits the array needs to store. The class provides get and set methods to access the values in the array. Listing 11-5 shows test code that uses the BitArray class.
import BitArray from "bitarray";
let bits = new BitArray(128);
bits.set(2, 1);
bits.set(3, bits.get(3) ? 0 : 1);

Listing 11-5.

The first argument to both get and set is the index of the bit in the array to get or set. The index of the first array element is 0. The final line of the example toggles the value of the bit at index 3.

Using Memory Allocated by ArrayBuffer

The implementation of BitArray in the $EXAMPLES/ch11-native/bitarray-arraybuffer example is shown in Listing 11-6. It begins by declaring the class in JavaScript. As in the earlier random-integer examples, the special XS @ syntax is used to connect the JavaScript function to a native C function. Notice that the constructor is implemented in JavaScript while the get and set methods are implemented in C. There’s no requirement that the class be implemented entirely in JavaScript or C; you can choose the language that works best for each method.
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.

The constructor allocates an ArrayBuffer to hold the bit values. Because the memory of a new ArrayBuffer is always initialized to 0, no further initialization is needed. The number of bits to store is divided by 8 to determine the number of bytes needed and then rounded up using Math.ceil to ensure that there are enough bytes allocated when the number of bits isn’t evenly divisible by 8. The ArrayBuffer is assigned to the buffer property of the BitArray instance. The native implementations of get and set access the memory using the buffer property.

The get Function

The native implementation of the get function , xs_bitarray_get, begins by retrieving the index of the bit, the first argument to the function. It uses the index argument to calculate byteIndex, the index of the byte that contains the bit, and bitIndex, the index of the bit within that byte.
int index = xsmcToInteger(xsArg(0));
int byteIndex = index >> 3;
int bitIndex = index & 0x07;
Next, xs_bitarray_get gets a pointer to the memory allocated by the ArrayBuffer stored in the buffer property. To do this, it first allocates one temporary JavaScript variable on the JavaScript stack, by calling xsmcVars with the argument 1 specifying the number of temporary variables.
xsmcVars(1);

Variables allocated using xsmcVars are accessed with xsVar, which is similar to xsArg but accesses local temporary variables instead of arguments to the function. The variables are automatically released when the native function that allocated them—in this case, xs_bitarray_get—returns. You should call xsmcVars only one time in a function, allocating all needed temporary variables at once.

The implementation of xs_bitarray_get retrieves a reference to its instance’s buffer property by calling xsmcGet. The value of the buffer property is placed in xsVar(0).
xsmcGet(xsVar(0), xsThis, xsID_buffer);

The second argument, xsThis here, tells xsmcGet which object you want to retrieve the property from. The third argument, xsID_buffer here, specifies that the name of the property you want to retrieve is buffer.

The preceding steps use many unfamiliar calls from XS in C. What they do is quite simple in JavaScript, and much more verbose to express in C. The JavaScript equivalent to the calls to xsmcVars and xsmcGet is as follows:
let var0;
var0 = this.buffer;
The buffer property is not a pointer to the memory buffer used by the ArrayBuffer instance; it’s a reference to the instance. Just as you use xsmcToInteger to convert a JavaScript value to an integer, you use xsmcToArrayBuffer to convert a JavaScript value to a native pointer. If the JavaScript value is not an ArrayBuffer instance, the call to xsmcToArrayBuffer throws an exception.
uint8_t *buffer = xsmcToArrayBuffer(xsVar(0));
Now that xs_bitarray_get has the buffer pointer, it uses the byteIndex and bitIndex values calculated earlier to read the bit and set the return value of the JavaScript function call to 0 or 1.
if (buffer[byteIndex] & (1 << bitIndex))
    xsmcSetInteger(xsResult, 1);
else
    xsmcSetInteger(xsResult, 0);

The set Function

The implementation of the set function (Listing 11-7) in xs_bitarray_set is very similar to the implementation of get. The values of byteIndex, bitIndex, and buffer are determined in the same way. The sole difference is that the value of the second argument, accessed with xsArg(1), is used to determine whether to set or clear the specified bit.
int value = xsmcToInteger(xsArg(1));
if (value)
    buffer[byteIndex] |= 1 << bitIndex;
else
    buffer[byteIndex] &= ~(1 << bitIndex);

Listing 11-7.

Security Vulnerability

This implementation of BitArray, using memory allocated by ArrayBuffer, works well, but it has a critical flaw that makes it unsuitable for safe use in real products. The get and set functions don’t verify that the index argument is inside the bounds of the memory allocated. This enables code using this implementation of BitArray to read and write arbitrary memory on embedded devices, which can cause a crash or be used as the basis of a privacy attack. There are multiple ways to solve this problem; the next section discusses one of them.

Using Memory Allocated by calloc

The implementation of BitArray in the $EXAMPLES/ch11-native/bitarray-calloc example solves the security problem presented by the bitarray-arraybuffer example as just discussed. It stores the number of bits allocated by the constructor and then validates the index passed to the get and set calls against that stored value.

The BitArray implementation in the bitarray-calloc example uses calloc instead of ArrayBuffer to allocate memory. The memory allocated by these two approaches comes from two different pools of memory: memory allocated by calloc is taken from the native system memory heap, whereas memory allocated by ArrayBuffer is inside the memory heap managed by XS. Some hosts are configured with more free space in one of these pools than the other, which may influence your decision about where to allocate memory from. A little bit less code is required to work with the memory allocated by calloc, though that difference may not be significant.

The bitarray-calloc example illustrates some important techniques for integrating native code into your project. In addition to a native constructor, this BitArray class also has a native destructor to perform cleanup when an instance of the class is garbage-collected. In XS, an object with a native destructor is called a host object .

The Class Declaration

Listing 11-8 shows the class declaration. This implementation of BitArray uses primarily native methods, unlike the implementation from the bitarray-arraybuffer example. Notice that the name of the native C function that implements the destructor, xs_bitarray_destructor, follows the declaration of the class name.
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.

The declarations of the get and set methods are the same as in the previous example, though the implementations are somewhat different.

The native constructor, destructor, and close functions are closely related. The next sections look at each in turn.

The Constructor

The native constructor in Listing 11-9 begins much like the JavaScript implementation, by calculating the number of bytes needed to store the requested number of bits and then allocating those bytes. The constructor allocates additional space, the size of an integer, to hold the bit count. If the allocation fails, the constructor calls xsUnknownError to throw an exception. The use of Unknown in the name xsUnknownError means that this a general-purpose error, which uses the JavaScript Error class, rather than a specific error such as 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.

Once the memory is allocated, the number of bits requested is stored at the start of the block. Because the memory is allocated using calloc, all bits are initialized to 0.

The call to xsmcSetHostData stores a reference to the memory allocated with this host object. This pointer is then available to all native methods of the object, through a call to xsmcGetHostData. You might be tempted to simply store the bytes pointer in a global variable; however, that approach fails when there’s more than one instance of the object, since the two objects can’t share a single C global variable. Using xsmcSetHostData to store the data pointer means that the implementation of BitArray supports an arbitrary number of simultaneous instances.

The Destructor

This is the first time in this book that you’ve seen a destructor. They’re common in C++ in working with objects, but they’re not a visible part of the JavaScript language. Instead, JavaScript automatically frees the memory used by objects when they’re garbage-collected. The JavaScript engine doesn’t know how to free the resources your host object allocated, such as the memory allocated with calloc. Therefore, you must implement a destructor.

For BitArray, the destructor (Listing 11-10) simply calls free to release the memory allocated by calloc.
void xs_bitarray_destructor(void *data)
{
    if (data)
        free(data);
}

Listing 11-10.

Here are some details to be aware of when implementing a destructor:
  • The function prototype of a destructor is different from regular native method calls. Instead of being passed a reference to the XS virtual machine as the, it has an argument that’s a data pointer, the same value you passed to xsmcSetHostData.

  • Because there’s no reference to the XS virtual machine (no the argument), you can’t make calls to XS in C. For example, you can’t call xsmcGetHostData, which is why the data pointer is always passed to the destructor function. That also means your destructor can’t create new objects, change the values of properties, or make function calls to the object. These limitations are necessary because the destructor is called from inside the garbage collector when such operations are unsafe.

  • The value of data may be NULL. This happens, for example, when the memory allocation in the constructor fails. As you’ll see in the next section, it also happens after the close method is called. Therefore, a good practice is to always check that the data argument isn’t NULL in your destructor before using it, as this example does.

The close Function

Chapters 3 and 5 contain examples of JavaScript objects that have a close method. This method releases any native resources—memory, file handles, network sockets, and so on—that the object owns. If the object isn’t explicitly closed, those resources are eventually released when the garbage collector determines that the object is no longer in use. However, there’s no way to know when the garbage collector will make that determination, which means it may be a very long time until the resources are freed. The close call solves this problem by giving code a way to explicitly free those resources.

Many host objects have an implementation of close like the one for BitArray (Listing 11-11).
void xs_bitarray_close(xsMachine *the)
{
    uint8_t *buffer = xsmcGetHostData(xsThis);
    xs_bitarray_destructor(buffer);
    xsmcSetHostData(xsThis, NULL);
}

Listing 11-11.

Here’s what these lines of code do:
  1. 1.

    The call to xsmcGetHostData retrieves the data pointer that was allocated in the constructor and associated with this object by the call to xsmcSetHostData.

     
  2. 2.

    The data pointer is passed to the destructor, which does the work of releasing the resources.

     
  3. 3.

    The call to xsmcSetHostData sets the saved data pointer to NULL. This ensures that, should close be called twice, the data pointer is freed only once.

     

The get and set Functions

This implementation of xs_bitarray_get calculates the bit and byte index values in the same way as in the ArrayBuffer version of get:
int index = xsmcToInteger(xsArg(0));
int byteIndex = index >> 3;
int bitIndex = index & 0x07;
As shown in Listing 11-12, xs_bitarray_get uses xsmcGetHostData to retrieve the data buffer. If the buffer is NULL, that indicates that the instance has already been closed, and get throws an error. The count of the number of bits allocated is stored in the first integer of the buffer; it’s extracted to the local variable bitCount, and then the buffer pointer is advanced to point to the bit array values.
uint8_t *buffer = xsmcGetHostData(xsThis);
int bitCount;
if (NULL == buffer)
    xsUnknownError("closed");
bitCount = *(int *)buffer;
buffer += sizeof(int);

Listing 11-12.

Before accessing the requested bit, the implementation first checks to see whether the value is in range. Because the index is a signed integer, it checks that it’s not greater than the number of bits allocated and that the index is not negative.
if ((index >= bitCount) || (index < 0))
    xsRangeError("invalid bit index");
With that check complete, reading the requested bit and setting the return value is identical to the previous version:
if (buffer[byteIndex] & (1 << bitIndex))
    xsmcSetInteger(xsResult, 1);
else
    xsmcSetInteger(xsResult, 0);

The implementation of set applies the same changes described for get in this section and so is not repeated here.

The length Property

The typed array classes include a length property in their instances which, as in instances of Array, indicates the number of elements in the array. This value is useful when you’re iterating over the array. Because this implementation of BitArray stores the number of bits allocated, it can also provide a length property.

The length property is implemented with a getter and a setter, two special kinds of JavaScript functions that are called when code accesses a property. Using the getter and setter for length enables you to write code like the following to initialize all bits to 1:
let bits = new BitArray(55);
for (let i = 0; i < bits.length; i++)
    bits.set(i, 1);
The first step in implementing the length property is to add the getter and setter to the BitArray class. Here the getter is the xs_bitarray_get_length native function. The length property is read-only, so instead of native code the setter implementation is a JavaScript function that always throws an exception. Notice that a host object may have JavaScript methods.
get length() @ "xs_bitarray_get_length";
set length(value) {
    throw new Error("read-only");
}
The implementation of xs_bitarray_get_length, shown in Listing 11-13, is straightforward. It uses xsmcGetHostData to retrieve the data pointer created in the constructor. If the instance has been closed—that is, if buffer is NULL—it throws an exception; otherwise, it sets the return value to the bit count extracted from the start of the data pointer.
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.

Advantages to This Approach

This second implementation of BitArray, using memory allocated by calloc, has many advantages over the first version:
  • It validates the input values, eliminating the ability of sloppy code to cause a crash and of malicious code to breach privacy.

  • It provides a length property, making it more convenient to work with.

  • It uses system memory to store the bit data, reducing the memory used in the memory heap managed by the JavaScript engine.

  • It uses the host data feature of XS in C to keep track of the memory buffer, requiring less code and running faster than using a JavaScript property.

Wi-Fi Signal Notifications

You’ve learned how to implement a class to manage native resources as a host object. This next example shows how to make calls from C code back to JavaScript and how to configure a host object using a dictionary. Both these techniques are used by many of the host objects in the Moddable SDK.

The $EXAMPLES/ch11-native/wifi-rssi-notify example implements the WiFiRSSINotify class, which lets you register callbacks to invoke when the Wi-Fi signal strength crosses above and below a specified threshold. You might use this in your product to give the user an indication of when Wi-Fi is likely to perform well or to throttle the amount of network traffic you generate when the signal is weak. The class could be implemented entirely in JavaScript using Timer together with the net module introduced in the “Getting Network Information” section of Chapter 3. This implementation using native code is a bit more efficient and provides a convenient starting point to show how to configure your host object from a dictionary and how to invoke callback functions.

When you run this example, you must specify a Wi-Fi access point for the microcontroller to connect to. That’s because RSSI measures the strength of the signal between your microcontroller and the access point it’s connected to; if there’s no connection, there’s nothing to measure. Here’s a typical command line to build and run this example:
> mcconfig -d -m -p esp32 ssid="My Wi-Fi" password="secret"

The Test Code

The WiFiRSSINotify class follows the common pattern of having a constructor that accepts a dictionary object of configuration options. Listing 11-14 shows test code in main.js that constructs an instance of this class. You need to specify the RSSI threshold below which the signal is considered weak and at which the signal is considered strong. An optional poll property configures how often the signal strength is checked; it’s set to 1,000 milliseconds in this example. The default polling frequency is 5,000 milliseconds.
import WiFiRSSINotify from "wifirssinotify";
let notify = new WiFiRSSINotify({
    threshold: -66,
    poll: 1000
});

Listing 11-14.

Once the notification instance is created, you can install an onWeakSignal and/or onStrongSignal callback, as shown in Listing 11-15. The onWeakSignal callback is invoked when the RSSI reaches or falls below the specified threshold, and onStrongSignal is invoked when the RSSI exceeds the threshold. The functions are called when the threshold is crossed, not each time the RSSI is polled. The current RSSI value is passed to the callback functions.
notify.onWeakSignal = function(rssi) {
    trace(`Weak Wi-Fi signal. RSSI ${rssi}. `);
}
notify.onStrongSignal = function(rssi) {
    trace(`Strong Wi-Fi signal. RSSI ${rssi}. `);
}

Listing 11-15.

The WiFiRSSINotify Class

The JavaScript class for WiFiRSSINotify is just a host object with a destructor, constructor, and close function all implemented in native code:
class WiFiRSSINotify @ "xs_wifirssinotify_destructor" {
    constructor(options) @ "xs_wifirssinotify_constructor";
    close() @ "xs_wifirssinotify_close";
}

Default functions for the onWeakSignal and onStrongSignal callbacks are not part of the class. Before invoking a callback, WiFiRSSINotify confirms that the instance has a property with the callback’s name.

The Native RSSINotifyRecord Structure

The WiFiRSSINotify class needs to maintain state to perform its work. That state is stored in a C language structure named RSSINotifyRecord , shown in Listing 11-16. You can think of this data structure as the C equivalent of the properties in a JavaScript instance.
struct RSSINotifyRecord {
    int         threshold;
    int         state;
    modTimer    timer;
    xsMachine   *the;
    xsSlot      obj;
};

Listing 11-16.

Before looking at the code that uses this data structure, it’s helpful to review how each field is used:
  • threshold – The RSSI threshold below which the signal is considered weak and at which the signal is considered strong.

  • state – The WiFiRSSINotify instance is always in one of three states: kRSSIUnknown when it’s created and then either kRSSIWeak or kRSSIStrong. This state is used to eliminate redundant callbacks when the state has not changed.

  • timer – A native timer used to implement polling.

  • the – A reference to the XS virtual machine that contains the WiFiRSSINotify instance. It’s used to invoke callbacks from the timer.

  • obj – A reference to the WiFiRSSINotify object that’s used to invoke callbacks from the timer. The type of this field, xsSlot, is used by XS to hold any JavaScript value. The xsArg, xsVar, and xsGet functions that you already know return values of type xsSlot.

Additional details about how these fields are used are provided in the following sections.

The implementation also defines RSSINotify as a pointer to RSSINotifyRecord for convenience:
typedef struct RSSINotifyRecord *RSSINotify;

The Constructor

The WiFiRSSINotify constructor begins by allocating storage for the RSSINotifyRecord structure. Once this structure is fully initialized, it’s attached to the object using xsmcSetHostData. As a rule, the data structure is not attached to the object before being initialized, to avoid having a partially initialized structure in case an error occurs during execution of the constructor.
RSSINotify rn = calloc(sizeof(RSSINotifyRecord), 1);
if (!rn)
    xsUnknownError("no memory");
Next, the constructor initializes the state, the, and obj fields:
rn->state = kRSSIUnknown;
rn->obj = xsThis;
rn->the = the;
The constructor performs several operations that may fail. When they fail, they throw an error that can be caught by the calling JavaScript code. Because the first operation the constructor performs is allocating memory, it needs to free that memory if an exception occurs. If it doesn’t do so, the memory is orphaned, causing a memory leak that could eventually lead to a system failure. To guard against this, the constructor surrounds those operations with xsTry, catching any exceptions with xsCatch. After catching the exception, the constructor frees the memory stored in rn and then uses xsThrow to throw the error again. In C, that use of xsTry and xsCatch has the structure shown in Listing 11-17.
xsTry {
    ...
}
xsCatch {
    free(rn);
    xsThrow(xsException);
}

Listing 11-17.

Recall that XS in C provides ways to access and implement basic JavaScript capabilities in your C code. The C code for xsTry-xsCatch is similar to the JavaScript version of the code, shown in Listing 11-18.
try {
    ...
}
catch(e) {
    ...
    throw e;
}

Listing 11-18.

The xsTry block begins by declaring a local variable, poll, to hold the requested polling interval from the dictionary argument and using xsmcVars to reserve space for a temporary value on the JavaScript stack:
int poll;
xsmcVars(1);
As shown in Listing 11-19, the constructor then calls xsmcHas to see if the dictionary argument contains the poll property. If it does, the property is retrieved, converted to an integer, and assigned to the local variable poll; otherwise, a default value of 5,000 is used.
if (xsmcHas(xsArg(0), xsID_poll)) {
    xsmcGet(xsVar(0), xsArg(0), xsID_poll);
    poll = xsmcToInteger(xsVar(0));
}
else
    poll = 5000;

Listing 11-19.

The xsmcHas function is similar to the in operator used in JavaScript. The preceding code is about the same as the JavaScript code in Listing 11-20.
let poll;
if ("poll" in options)
    poll = options.poll;
else
    poll = 5000;

Listing 11-20.

The constructor next calls xsmcHas again, this time to confirm that the required threshold property is present. If not, it throws an error; otherwise, the JavaScript threshold property is retrieved, converted to an integer, and assigned to the threshold field of rn.
if (!xsmcHas(xsArg(0), xsID_threshold))
    xsUnknownError("threshold required");
xsmcGet(xsVar(0), xsArg(0), xsID_threshold);
rn->threshold = xsmcToInteger(xsVar(0));
Finally, the xsTry block allocates a native timer using modTimerAdd from the Moddable SDK. You may use another timer mechanism here, one specific to your microcontroller. This code uses modTimerAdd for convenience, as it’s available for both ESP32 and ESP8266 devices. If the timer can’t be allocated—for example, because there’s insufficient memory available—the constructor throws an exception.
rn->timer = modTimerAdd(1, poll, checkRSSI, &rn, sizeof(rn));
if (!rn->timer)
    xsUnknownError("no timer");

The call to modTimerAdd creates a timer that first fires after 1 millisecond and then fires at the interval specified by poll. When the timer fires, it calls the checkRSSI native function, passing it the value of rn. A later section shows how the native callback retrieves this value and invokes the JavaScript callbacks.

That’s the end of the xsTry block. Even in this relatively simple object, there are two exceptions that the constructor itself generates. In addition, the calls to xsmcToInteger throw exceptions when passed a value that can’t be converted to an integer. These many potentials for exceptions make it important for the constructor to ensure that no memory or other resources are orphaned if an exception is thrown. Using xsTry with xsCatch often helps with this.

There are two more steps remaining in the constructor. The first is to store the rn data pointer with the object:
xsmcSetHostData(xsThis, rn);
The second is to ensure that the object is garbage-collected only after the JavaScript code calls close on the object. This behavior is common for JavaScript host objects that support callbacks. To do this, the constructor calls the xsRemember function with the object stored in the RSSINotifyRecord.
xsRemember(rn->obj);

You can only pass xsRemember a value in storage that your code allocated. If you call xsRemember with values such as xsThis, xsArg(0), xsVar(1), or other XS-provided values, it silently fails. As you might expect, there’s a corresponding xsForget call that needs to be called in close. The memory where the object is stored, rn->obj here, must persist until xsForget is called and therefore must not be a local variable in the constructor.

The Destructor

The destructor for WiFiRSSINotify (Listing 11-21) is similar to the other destructors in this chapter, with the addition of code to free the timer allocated in the constructor. To access the timer in the RSSINotifyRecord structure, the data pointer argument is cast to an RSSINotify pointer. The constructor implementation guarantees that the timer field is never NULL in the destructor when rn is non-NULL. Therefore, there’s no need to check that rn->timer is non-NULL before calling modTimerRemove.
void xs_wifirssinotify_destructor(void *data)
{
    RSSINotify rn = data;
    if (rn) {
        modTimerRemove(rn->timer);
        free(rn);
    }
}

Listing 11-21.

The close Function

The close method of WiFiRSSINotify (Listing 11-22) also follows a familiar pattern. However, in addition it must call xsForget to make the object eligible for garbage collection, counteracting the call to xsRemember in the constructor. Because the call to xsForget accesses the obj field of rn, the close implementation must guard against being called more than once by checking that xsmcGetHostData returns a non-NULL value.
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.

The call to xsForget can’t be made in the destructor because the destructor can’t use XS in C, as explained previously.

The Callback

The checkRSSI function , shown in Listing 11-23, is at the heart of the WiFiRSSINotify class. It’s invoked at the polling interval to detect when the RSSI value crosses the specified threshold value. The function begins by recovering the value of rn, the pointer to the RSSINotifyRecord structure allocated in the constructor. Because the checkRSSI callback isn’t called directly by XS, but by modTimer, the pointer can’t be retrieved using xsmcGetHostData as usual, but is instead retrieved by dereferencing the refcon argument.
void checkRSSI(modTimer timer, void *refcon, int refconSize)
{
    RSSINotify rn = *(RSSINotify *)refcon;
    ...
}

Listing 11-23.

The next step is to get the current RSSI value, which is done differently on the ESP32 and the ESP8266. Listing 11-24 has conditional cases for each, and an error for other targets.
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.

As shown in Listing 11-25, the polling function uses the current RSSI value to decide if it’s necessary to invoke either the onStrongSignal or the onWeakSignal JavaScript callback function. It checks to see if the current value is above or below the specified threshold stored in rn->threshold. If the RSSI value is on the same side of the threshold as the previous check, checkRSSI returns immediately; otherwise, it updates rn->state to the new state and assigns the ID of the callback to invoke, either xsID_onStrongSignal or xsID_onWeakSignal, to the local variable 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.

Invoking a JavaScript function from native code requires a valid JavaScript stack frame. When a native method is called from JavaScript, XS has already created that stack frame. The checkRSSI function isn’t called by XS, but by modTimer, and therefore must set up the stack frame itself. It does this by calling xsBeginHost before the callback. It calls xsEndHost afterward to remove the stack frame that xsBeginHost creates. Both functions take the, a reference to the JavaScript virtual machine, as their sole argument. Between xsBeginHost and xsEndHost, you can make calls to XS in C as usual.

The code in Listing 11-26 creates a temporary JavaScript variable using xsmcVars(1) and assigns it an integer value of rssi using xsmcSetInteger. It then calls xsmcHas to confirm that the object has the callback function. If it does, it uses xsCall to invoke the callback function, passing the RSSI value stored in xsVar(0).
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.

You use xsCall1 to call functions with one argument (and xsCall0 to call functions with no arguments, xsCall2 for functions with two arguments, and so on, up to xsCall9).

Additional Techniques

You now know how to invoke native code from JavaScript code and JavaScript code from native code, giving you the power to integrate native code and scripts in whatever way makes the most sense for your project. This section briefly introduces several important topics that you may find useful when integrating native code into your own JavaScript-powered products. Along with discussing a variety of techniques to help you build the bridge between your native and JavaScript code, it includes warnings about some common mistakes.

Debugging Native Code

As you develop increasingly complex native code, you may need to debug that code. Although you may not have a native debugger available, your code can interact with xsbug.

A common debugging technique is to send diagnostic output to the debug console. In embedded JavaScript, you use trace to do this. Using XS in C, you can do the same with xsTrace.
xsTrace("about to get RSSI ");
The argument to xsTrace is a string, making it convenient to output the progress of a function. If you need to output more detailed information, use xsLog, which provides printf-style functionality.
xsLog("RSSI is %d. ", rssi);
Both xsTrace and xsLog require a valid XS stack frame; therefore, they must be called either from a method invoked directly by XS or between an xsBeginHost-xsEndHost pair. For example, to output the current RSSI level to the debug console from the checkRSSI callback, you use this code:
xsBeginHost(rn->the);
    xsLog("RSSI is %d. ", rssi);
xsEndHost(rn->the);
It can be useful to trigger a breakpoint in xsbug from your native code to see the stack frames leading up to your native function being called and the arguments passed to it. Although you can’t set a breakpoint in native code using xsbug, you can trigger a breakpoint by calling xsDebugger in your C code.
xsDebugger();

Accessing Global Variables

Your code can get and set the value of global variables directly. All global variables are part of the global object, which is accessed in JavaScript using globalThis. In XS in C, the global object is available to your native code as xsGlobal. You can use xsGlobal in your native code like any other object. For example, you use the xsmcSet* functions to assign values to a global variable, and the following lines set the global variable status to 0x8012:
xsmcSetInteger(xsVar(0), 0x8012);
xsmcSet(xsGlobal, xsID_status, xsVar(0));
You get the value of a global using xsmcGet:
xsmcGet(xsVar(0), xsGlobal, xsID_status);
int status = xsmcToInteger(xsVar(0));
The following code checks to see if there’s a global variable named onRestart. If there is, it calls the function stored in the onRestart global.
if (xsmcHas(xsGlobal, xsID_onRestart))
    xsCall0(xsGlobal, xsID_onRestart);

Getting a Function’s Return Value

When you use the family of xsCall* functions to invoke a JavaScript function from C, you can access the return value by assigning the result to a JavaScript value. For example, the following code calls the function on the callback property of this and traces the result to the console:
xsmcVars(1);
xsVar(0) = xsCall0(xsThis, xsID_callback);
xsTrace(xsVar(0));

Getting Values

The examples in this chapter use xsmcToInteger to get an integer value from a JavaScript value. There are similar functions for getting a boolean, floating-point number, string, and ArrayBuffer from a JavaScript value, as shown in Listing 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.

All of these functions fail if the JavaScript value can’t be converted to the requested type. For example, xsmcToArrayBuffer fails if the value is a string.

Special care is required when working with the pointers to strings and with ArrayBuffer pointers. See the section “Ensuring Your Buffer Pointers Are Valid” for details.

Setting Values

You’ve already seen how to use xsmcSetInteger to set a JavaScript property to an integer value. In addition, there are xsmcSet* functions for setting other basic JavaScript values, as shown in Listing 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.

You can also create objects using XS in C. The following code creates an ArrayBuffer object of 16 bytes and sets the first byte to 1:
xsmcSetArrayBuffer(xsResult, NULL, 16);
uint8_t *buffer = xsmcToArrayBuffer(xsResult);
buffer[0] = 1;
Listing 11-29 creates an object and adds several properties to it. Using this approach, your code can return objects just as the next method of the File class does.
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.

The JavaScript equivalent of that code is as follows:
return {name: "test.txt", length: 1024};
Listing 11-30 creates an array with eight elements and uses xsmcSet to set each array element to the square of its index. You’ve already seen xsmcSet used to set the value of a property of an object; here it’s used to set the value of an array element by passing the element’s index instead of an xsID_*-style symbol identifier.
xsmcSetNewArray(xsResult, 8);
for (i = 0; i < 8; i++) {
    xsmcSetInteger(xsVar(0), i * i);
    xsmcSet(xsResult, i, xsVar(0));
}

Listing 11-30.

Determining a Value’s Type

Your native code sometimes needs to know the type of a JavaScript value. For example, some functions change their behavior depending on whether an argument is an object or a number. You use xsmcTypeOf to determine the basic type of a value.
int typeOf = xsmcTypeOf(xsArg(1));
if (xsStringType == typeOf)
    ...;

The types returned by xsmcTypeOf are xsUndefinedType, xsNullType, xsBooleanType, xsIntegerType, xsNumberType, xsStringType, and xsReferenceType. Most of these correspond directly to JavaScript types you’re already familiar with. Notice, however, that there are types for both integers and numbers (floating-point values). While JavaScript itself uses the Number type for both, XS stores them as distinct types, as an optimization. If your native code checks whether a JavaScript value is of type Number, it needs to check for both xsIntegerType and xsNumberType.

The type xsReferenceType corresponds to a JavaScript object. This single type constant is used for all JavaScript objects. You use the xsmcIsInstanceOf function to determine whether the object is an instance of a particular class. The type xsmcIsInstanceOf is similar to JavaScript’s instanceof operator. XS defines values for built-in objects—for example, xsArrayPrototype. The following code sets the variable isArray to 1 if the first argument to the native method is an array or 0 if it’s not:
int typeOf = xsmcTypeOf(xsArg(0));
int isArray = (xsReferenceType == typeOf) &&
              xsmcIsInstanceOf(xsArg(0), xsArrayPrototype);
The xsmcIsInstanceOf function returns true if the object is a subclass of the specified type. For example, the section “Accessing Values of a Data View” in Chapter 2 defines the Header class as a subclass of DataView. Passing an instance of Header to the following call returns true:
if (xsmcIsInstanceOf(xsArg(0), xsDataViewPrototype))
    ...;    // is a data view

Other useful prototypes defined by XS that may be used with xsmcIsInstanceOf include xsFunctionPrototype, xsDatePrototype, xsErrorPrototype, and xsTypedArrayPrototype. For a complete list, see the xs.h header file in the Moddable SDK.

Working with Strings

Strings are commonly used in JavaScript. Because XS stores them in UTF-8 encoding, strings are convenient to work with in C. Here are a few details to keep in mind:
  • You’re guaranteed that strings you receive from XS are valid UTF-8. You must ensure that any strings you pass to XS are also valid UTF-8.

  • XS treats a null character (ASCII 0) as the end of the string, so don’t include any null characters in your strings. (Since the C language also uses the null character to terminate a string, this should be familiar.) Your code probably doesn’t intentionally create invalid UTF-8 strings or include null characters in a string, but they can sneak in when you import strings from a file or a network connection; it’s a good practice to validate these strings before passing them to XS.

  • In JavaScript, strings are read-only. No functions are provided to change the content of a string. You could choose to break this rule in your native code—but don’t! Doing so would break a fundamental assumption that JavaScript programmers rely on. Furthermore, it could cause a crash, as some strings are stored in read-only flash memory and attempting to write to them causes the microcontroller to reset.

  • The string pointer returned from xsmcToString can be invalidated when you make other calls using XS in C. The next section explains the details.

Ensuring Your Buffer Pointers Are Valid

When you call xsmcToString or xsmcToArrayBuffer, they don’t return a copy of the data; they return a pointer into an XS data structure. This behavior is important on microcontrollers, where the extra time and memory required to make a copy are unacceptable. The pointer may become invalid when you make a call to XS in C that causes the garbage collector to run. The garbage collector cannot free the ArrayBuffer or string, because they’re in use. However, the garbage collector may move the data structure when it compacts the memory heap to make more space by combining areas of free space.

With some care, as in the following approaches, you can avoid any problems when the garbage collector compacts the heap:
  • Never use a pointer returned by XS in C after making another call to XS in C. This may seem challenging, but all the examples so far in this chapter have done exactly that.

  • Make a copy of the data. While this approach is not optimal, it’s occasionally necessary.

Two functions can help when you’re working with pointers to strings and ArrayBuffer pointers. The xsmcToStringBuffer function is similar to xsmcToString, but instead of returning a string pointer it copies the string to a buffer. If the buffer is too small to hold the string, it throws a RangeError error.
char str[40];
xsmcToStringBuffer(xsArg(0), str, sizeof(str));
The xsmcGetArrayBufferData function copies all or part of an ArrayBuffer into another buffer. The second argument is the ArrayBuffer offset (in bytes) from which to begin copying the data, the third argument is the destination buffer, and the final argument is the size of the destination buffer in bytes. This example copies five bytes starting at offset 10 from an ArrayBuffer to the local variable buffer.
uint8_t buffer[5];
xsmcGetArrayBufferData(xsResult, 10, buffer, sizeof(buffer));

Integrating with C++

XS in C enables you to bridge not only between C and JavaScript code but also between C++ and JavaScript code. Although both JavaScript and C++ support objects, the details of how they implement objects and their features are quite different. Therefore, it’s usually unrealistic to try to create a direct mapping between your C++ classes and your JavaScript classes. Instead, design your JavaScript classes to make sense to JavaScript programmers and your C++ classes to make sense to C++ programmers. The bridge code you write using XS in C can translate between the two.

Using Threads

JavaScript is a single-threaded language; for this reason, the XS JavaScript engine is also single-threaded. This means that all calls to a single JavaScript virtual machine, as represented to native code by the, should be made from the same thread or task. You shouldn’t call XS in C from an interrupt or a thread other than the one that created the virtual machine.

Techniques that provide multitasking execution of JavaScript code, such as the Web Workers class, are built outside the JavaScript language. The Moddable SDK supports a subset of the Web Workers class on the ESP32, which enables several JavaScript virtual machines to coexist, each in their own thread. Each virtual machine is single-threaded, but several machines may run in parallel. The implementation of Web Workers for ESP32 respects the requirement that each individual JavaScript virtual machine is single-threaded.

Conclusion

The ability to bridge between JavaScript and native code using the XS in C API opens the door to many new possibilities for your projects. It enables you to optimize memory use, improve performance, reuse existing C and C++ code libraries, and access unique hardware capabilities. However, using XS in C is considerably more difficult than working in JavaScript, and consequently more error-prone. As a rule, using as little native code as practical tends to minimize the risks.

To help you learn more about working with XS in C, these two excellent resources are available:
  • The XS in C documentation is a complete reference to the API. It’s part of the Moddable SDK.

  • All the classes in the Moddable SDK that access native capabilities are implemented using XS in C. If you’re curious about how they work, the source code is there for you to read and learn from.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.43.140