Skip to content

Commit 81b01d2

Browse files
Add TextEncoder polyfill (WHATWG Encoding Standard) (#171)
Adds a `TextEncoder` polyfill that mirrors the existing `TextDecoder` polyfill in this repository, so non-Babylon-Native consumers can get both halves of the [WHATWG Encoding Standard](https://encoding.spec.whatwg.org/) from `JsRuntimeHost` without having to pull in or duplicate a separate implementation. `TextEncoder` is needed by older Chakra-based runtimes where the global is not built in. Modern V8 / JSC / Hermes runtimes already expose it natively, in which case `Initialize()` is a no-op. ## Surface - `TextEncoder()` constructor — UTF-8 only, per spec - `encoding` accessor — always `"utf-8"` - `encode(input)` → `Uint8Array` - `encodeInto(source, destination)` → `{ read, written }` - `read` is in UTF-16 code units, so a 4-byte UTF-8 sequence (code point outside the BMP) reports `2` - Multi-byte UTF-8 sequences are never split across the destination boundary ## Build / wiring - New gated option `JSRUNTIMEHOST_POLYFILL_TEXTENCODER` (default `ON`), matching the pattern used by every other polyfill in this repo. - New `Polyfills/TextEncoder/` library with the same layout as `Polyfills/TextDecoder/` (`CMakeLists.txt`, `Include/Babylon/Polyfills/TextEncoder.h`, `Source/TextEncoder.cpp`, `README.md`). - Linked into the unit-test executable (Windows + Android) and initialized in `Tests/UnitTests/Shared/Shared.cpp` next to `TextDecoder::Initialize`. ## Tests Adds a new `describe("TextEncoder", ...)` block in `Tests/UnitTests/Scripts/tests.ts` covering: - `encoding === "utf-8"` - `encode()` on ASCII, undefined / no-arg, multi-byte UTF-8 (e.g. `"é"` → `[0xC3, 0xA9]`), and embedded null bytes - `encodeInto()` happy path, refusal to split a multi-byte sequence when the destination is too small, and the surrogate-pair `read` semantics for `U+1F600` - `encodeInto()` `TypeError` when the destination is not a `Uint8Array` ## Motivation Surfaced during the BabylonNative review of [BabylonJS/BabylonNative#1708](BabylonJS/BabylonNative#1708), where this polyfill originally lived. Per @bghgary's review feedback, `TextEncoder` belongs alongside `TextDecoder` in JsRuntimeHost so it's available to any consumer of this runtime host, not just BN. --------- Co-authored-by: Branimir Karadžić (via Copilot) <223556219+Copilot@users.noreply.github.com>
1 parent 1e9b17e commit 81b01d2

10 files changed

Lines changed: 171 additions & 0 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ option(JSRUNTIMEHOST_POLYFILL_WEBSOCKET "Include JsRuntimeHost Polyfill WebSocke
8282
option(JSRUNTIMEHOST_POLYFILL_BLOB "Include JsRuntimeHost Polyfill Blob." ON)
8383
option(JSRUNTIMEHOST_POLYFILL_PERFORMANCE "Include JsRuntimeHost Polyfill Performance." ON)
8484
option(JSRUNTIMEHOST_POLYFILL_TEXTDECODER "Include JsRuntimeHost Polyfill TextDecoder." ON)
85+
option(JSRUNTIMEHOST_POLYFILL_TEXTENCODER "Include JsRuntimeHost Polyfill TextEncoder." ON)
8586

8687
# Sanitizers
8788
option(ENABLE_SANITIZERS "Enable AddressSanitizer and UBSan" OFF)

Polyfills/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,8 @@ endif()
3232

3333
if(JSRUNTIMEHOST_POLYFILL_TEXTDECODER)
3434
add_subdirectory(TextDecoder)
35+
endif()
36+
37+
if(JSRUNTIMEHOST_POLYFILL_TEXTENCODER)
38+
add_subdirectory(TextEncoder)
3539
endif()
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
set(SOURCES
2+
"Include/Babylon/Polyfills/TextEncoder.h"
3+
"Source/TextEncoder.cpp")
4+
5+
add_library(TextEncoder ${SOURCES})
6+
warnings_as_errors(TextEncoder)
7+
8+
target_include_directories(TextEncoder PUBLIC "Include")
9+
10+
target_link_libraries(TextEncoder
11+
PUBLIC napi
12+
PUBLIC Foundation)
13+
14+
set_property(TARGET TextEncoder PROPERTY FOLDER Polyfills)
15+
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES})
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#pragma once
2+
3+
#include <napi/env.h>
4+
#include <Babylon/Api.h>
5+
6+
namespace Babylon::Polyfills::TextEncoder
7+
{
8+
void BABYLON_API Initialize(Napi::Env env);
9+
}

Polyfills/TextEncoder/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# TextEncoder Polyfill
2+
3+
A C++ implementation of the [WHATWG Encoding API](https://encoding.spec.whatwg.org/) `TextEncoder` interface for use in Babylon Native JavaScript runtimes via [Napi](https://github.com/nodejs/node-addon-api).
4+
5+
This is the encoding counterpart to the `TextDecoder` polyfill in this same repository. Both polyfills exist primarily for older Chakra-based runtimes where the WHATWG Encoding Standard globals are not built in. On modern V8 / JSC / Hermes runtimes the constructor is already exposed and `Initialize()` is a no-op.
6+
7+
## Current State
8+
9+
### Supported
10+
11+
- Constructing `TextEncoder` with no argument (UTF-8 is the only encoding the spec mandates).
12+
- The `encoding` accessor (always returns `"utf-8"`).
13+
- `encode(input)` — returns a `Uint8Array` containing the UTF-8 bytes of `input`. Calling with no argument or `undefined` returns an empty `Uint8Array` (matches the spec, which defaults `input` to `""`).
14+
15+
### Not Supported
16+
17+
- Encodings other than UTF-8 — the `TextEncoder` constructor in the spec only accepts UTF-8 anyway, so this is not a deviation.
18+
- `encodeInto(source, destination)` — not implemented. Babylon.js does not call this entry point; the (substantially more involved) UTF-16-code-unit accounting it would require is not justified by any current consumer. If a future consumer needs it, it can be added at that time.
19+
20+
## Usage
21+
22+
```javascript
23+
const encoder = new TextEncoder();
24+
encoder.encoding; // "utf-8"
25+
26+
encoder.encode("Hello"); // Uint8Array(5) [72,101,108,108,111]
27+
encoder.encode(); // Uint8Array(0) []
28+
```
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#include <Babylon/Polyfills/TextEncoder.h>
2+
3+
#include <napi/napi.h>
4+
5+
#include <cstring>
6+
#include <string>
7+
8+
namespace
9+
{
10+
class TextEncoder final : public Napi::ObjectWrap<TextEncoder>
11+
{
12+
public:
13+
static void Initialize(Napi::Env env)
14+
{
15+
Napi::HandleScope scope{env};
16+
17+
static constexpr auto JS_TEXTENCODER_CONSTRUCTOR_NAME = "TextEncoder";
18+
if (env.Global().Get(JS_TEXTENCODER_CONSTRUCTOR_NAME).IsUndefined())
19+
{
20+
Napi::Function func = DefineClass(
21+
env,
22+
JS_TEXTENCODER_CONSTRUCTOR_NAME,
23+
{
24+
InstanceAccessor("encoding", &TextEncoder::Encoding, nullptr),
25+
InstanceMethod("encode", &TextEncoder::Encode),
26+
});
27+
28+
env.Global().Set(JS_TEXTENCODER_CONSTRUCTOR_NAME, func);
29+
}
30+
}
31+
32+
explicit TextEncoder(const Napi::CallbackInfo& info)
33+
: Napi::ObjectWrap<TextEncoder>{info}
34+
{
35+
// The TextEncoder constructor takes no arguments. The encoding is
36+
// always UTF-8 per the WHATWG Encoding Standard.
37+
}
38+
39+
private:
40+
Napi::Value Encoding(const Napi::CallbackInfo& info)
41+
{
42+
return Napi::String::New(info.Env(), "utf-8");
43+
}
44+
45+
// encode(input = "") - returns a Uint8Array containing the UTF-8 bytes.
46+
// Per spec, undefined input defaults to the empty string (not "undefined").
47+
Napi::Value Encode(const Napi::CallbackInfo& info)
48+
{
49+
auto env = info.Env();
50+
std::string utf8;
51+
if (info.Length() > 0 && !info[0].IsUndefined())
52+
{
53+
utf8 = info[0].ToString().Utf8Value();
54+
}
55+
56+
auto bytes = Napi::Uint8Array::New(env, utf8.size());
57+
if (!utf8.empty())
58+
{
59+
std::memcpy(bytes.Data(), utf8.data(), utf8.size());
60+
}
61+
return bytes;
62+
}
63+
};
64+
}
65+
66+
namespace Babylon::Polyfills::TextEncoder
67+
{
68+
void BABYLON_API Initialize(Napi::Env env)
69+
{
70+
::TextEncoder::Initialize(env);
71+
}
72+
}

Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,5 @@ target_link_libraries(UnitTestsJNI
4242
PRIVATE gtest_main
4343
PRIVATE Blob
4444
PRIVATE TextDecoder
45+
PRIVATE TextEncoder
4546
PRIVATE Performance)

Tests/UnitTests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ target_link_libraries(UnitTests
6161
PRIVATE Blob
6262
PRIVATE Performance
6363
PRIVATE TextDecoder
64+
PRIVATE TextEncoder
6465
${ADDITIONAL_LIBRARIES})
6566

6667
# See https://gitlab.kitware.com/cmake/cmake/-/issues/23543

Tests/UnitTests/Scripts/tests.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1356,6 +1356,44 @@ describe("TextDecoder", function () {
13561356
});
13571357
});
13581358

1359+
describe("TextEncoder", function () {
1360+
it("should expose encoding === 'utf-8'", function () {
1361+
const encoder = new TextEncoder();
1362+
expect(encoder.encoding).to.equal("utf-8");
1363+
});
1364+
1365+
it("should encode an ASCII string into UTF-8 bytes", function () {
1366+
const encoder = new TextEncoder();
1367+
const bytes = encoder.encode("Hello");
1368+
expect(Array.from(bytes)).to.eql([72, 101, 108, 108, 111]);
1369+
});
1370+
1371+
it("should return an empty Uint8Array when called with no argument", function () {
1372+
const encoder = new TextEncoder();
1373+
const bytes = encoder.encode();
1374+
expect(bytes.length).to.equal(0);
1375+
});
1376+
1377+
it("should return an empty Uint8Array for undefined input", function () {
1378+
const encoder = new TextEncoder();
1379+
const bytes = encoder.encode(undefined);
1380+
expect(bytes.length).to.equal(0);
1381+
});
1382+
1383+
it("should encode a multi-byte UTF-8 string", function () {
1384+
const encoder = new TextEncoder();
1385+
// "é" is U+00E9 -> 0xC3 0xA9 in UTF-8
1386+
const bytes = encoder.encode("é");
1387+
expect(Array.from(bytes)).to.eql([0xC3, 0xA9]);
1388+
});
1389+
1390+
it("should encode a string containing a null byte", function () {
1391+
const encoder = new TextEncoder();
1392+
const bytes = encoder.encode("H\0i");
1393+
expect(Array.from(bytes)).to.eql([72, 0, 105]);
1394+
});
1395+
});
1396+
13591397
function runTests() {
13601398
mocha.run((failures: number) => {
13611399
// Test program will wait for code to be set before exiting

Tests/UnitTests/Shared/Shared.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <Babylon/Polyfills/XMLHttpRequest.h>
1111
#include <Babylon/Polyfills/Blob.h>
1212
#include <Babylon/Polyfills/TextDecoder.h>
13+
#include <Babylon/Polyfills/TextEncoder.h>
1314
#include <gtest/gtest.h>
1415
#include <arcana/threading/blocking_concurrent_queue.h>
1516
#include <atomic>
@@ -83,6 +84,7 @@ TEST(JavaScript, All)
8384
Babylon::Polyfills::XMLHttpRequest::Initialize(env);
8485
Babylon::Polyfills::Blob::Initialize(env);
8586
Babylon::Polyfills::TextDecoder::Initialize(env);
87+
Babylon::Polyfills::TextEncoder::Initialize(env);
8688

8789
auto setExitCodeCallback = Napi::Function::New(
8890
env, [&exitCodePromise](const Napi::CallbackInfo& info) {

0 commit comments

Comments
 (0)