diff --git a/docs/docs/stdlib-filesystem.md b/docs/docs/stdlib-filesystem.md index 4d216c32..ec17d1c2 100644 --- a/docs/docs/stdlib-filesystem.md +++ b/docs/docs/stdlib-filesystem.md @@ -92,7 +92,7 @@ const binary = fs.readFileSync('image.png') as ArrayBuffer; const bytes = new Uint8Array(binary); ``` -#### `writeFileSync(path: string, data: ArrayBuffer | string): void` +#### `writeFileSync(path: string, data: ArrayBuffer | Uint8Array | string): void` Synchronously writes data to a file, creating it if it doesn't exist or overwriting if it does. @@ -106,7 +106,7 @@ fs.writeFileSync('output.txt', 'Hello, World!'); // Write binary const imageData = new Uint8Array([...]); // image bytes -fs.writeFileSync('output.png', imageData.buffer); +fs.writeFileSync('output.png', imageData); // Write JSON const config = { setting1: true, setting2: 'value' }; @@ -352,4 +352,3 @@ const mockFs = { currentWorkingDirectory: jest.fn(() => '/mock/path') }; ``` - diff --git a/libs/utils/src/utils/encoding/Base64Utils.cpp b/libs/utils/src/utils/encoding/Base64Utils.cpp index 7003e7d1..dbe53189 100644 --- a/libs/utils/src/utils/encoding/Base64Utils.cpp +++ b/libs/utils/src/utils/encoding/Base64Utils.cpp @@ -1,10 +1,9 @@ #include "utils/encoding/Base64Utils.hpp" -#include #include #include -#include +#include #include #include @@ -39,15 +38,10 @@ std::string uint64ToBase64(uint64_t data) { return binaryToBase64(bytes.data(), bytes.size()); } -bool base64ToBinaryInternal(const char* encodedString, size_t inSize, std::vector* ret) { - // Strip out newlines since the decoder returns an error code (0) if it finds them within the encoded string. - std::string noNewlines(encodedString, inSize); - boost::algorithm::replace_all(noNewlines, "\n", ""); - boost::algorithm::replace_all(noNewlines, "\r", ""); - +static bool decodePreparedBase64(const std::string& preparedBase64, std::vector* ret) { // Determine the max array length we'll need given the string length size_t maxOutSize = 0; - if (EVP_DecodedLength(&maxOutSize, noNewlines.length()) != 1 || maxOutSize == 0) { + if (EVP_DecodedLength(&maxOutSize, preparedBase64.length()) != 1 || maxOutSize == 0) { return false; } @@ -56,8 +50,8 @@ bool base64ToBinaryInternal(const char* encodedString, size_t inSize, std::vecto if (EVP_DecodeBase64(reinterpret_cast(ret->data()), &outSize, maxOutSize, - reinterpret_cast(noNewlines.c_str()), - noNewlines.length()) != 1) { + reinterpret_cast(preparedBase64.c_str()), + preparedBase64.length()) != 1) { // make it empty to indicate error ret->resize(0); return false; @@ -67,6 +61,17 @@ bool base64ToBinaryInternal(const char* encodedString, size_t inSize, std::vecto return true; } +static bool base64ToBinaryInternal(const char* encodedString, size_t inSize, std::vector* ret) { + // Strip out newlines since the decoder returns an error code (0) if it finds them within the encoded string. + std::string noNewlines; + noNewlines.reserve(inSize); + noNewlines.append(encodedString, inSize); + noNewlines.erase(std::remove(noNewlines.begin(), noNewlines.end(), '\n'), noNewlines.end()); + noNewlines.erase(std::remove(noNewlines.begin(), noNewlines.end(), '\r'), noNewlines.end()); + + return decodePreparedBase64(noNewlines, ret); +} + std::vector base64ToBinary(std::string_view base64) { std::vector decodedData; base64ToBinaryInternal(base64.data(), base64.size(), &decodedData); @@ -77,6 +82,20 @@ bool base64ToBinary(std::string_view base64, std::vector& decodedData) return base64ToBinaryInternal(base64.data(), base64.size(), &decodedData); } +bool base64UrlToBinary(std::string_view base64url, std::vector& decodedData) { + std::string standardBase64; + standardBase64.reserve(base64url.size() + 4); + standardBase64.append(base64url.data(), base64url.size()); + standardBase64.erase(std::remove(standardBase64.begin(), standardBase64.end(), '\n'), standardBase64.end()); + standardBase64.erase(std::remove(standardBase64.begin(), standardBase64.end(), '\r'), standardBase64.end()); + base64UrlToBase64InPlace(standardBase64); + if (standardBase64.empty()) { + decodedData.clear(); + return true; + } + return decodePreparedBase64(standardBase64, &decodedData); +} + uint64_t base64ToUInt64(const std::string& base64) { std::vector bytes = base64ToBinary(base64); uint64_t retVal = 0; @@ -90,44 +109,52 @@ uint64_t base64ToUInt64(const std::string& base64) { } std::string base64UrlToBase64(const std::string& base64url) { - std::string temp; - temp.reserve(base64url.size() + 4); + std::string temp(base64url); + base64UrlToBase64InPlace(temp); + return temp; +} +void base64UrlToBase64InPlace(std::string& base64url) { // change Base64 alphabet from urlsafe version to standard - for (const auto& c : base64url) { + for (auto& c : base64url) { if (c == '-') { - temp += '+'; + c = '+'; } else if (c == '_') { - temp += '/'; - } else { - temp += c; + c = '/'; } } // add padding if ((base64url.size() % 4) != 0u) { - int toAppend = 4 - static_cast(base64url.size() % 4); - for (int i = 0; i < toAppend; i++) { - temp += '='; - } + auto toAppend = 4 - static_cast(base64url.size() % 4); + base64url.append(toAppend, '='); } - - return temp; } std::string base64ToBase64Url(const std::string& base64) { std::string temp(base64); + base64ToBase64UrlInPlace(temp); + return temp; +} +void base64ToBase64UrlInPlace(std::string& base64) { // remove padding - size_t found = temp.find_last_not_of('='); - if (found == std::string::npos) - return ""; + size_t found = base64.find_last_not_of('='); + if (found == std::string::npos) { + base64.clear(); + return; + } // change Base64 alphabet from standard version to urlsafe - boost::algorithm::replace_all(temp, "+", "-"); - boost::algorithm::replace_all(temp, "/", "_"); + for (auto& c : base64) { + if (c == '+') { + c = '-'; + } else if (c == '/') { + c = '_'; + } + } - return temp.substr(0, found + 1); + base64.resize(found + 1); } } // namespace snap::utils::encoding diff --git a/libs/utils/src/utils/encoding/Base64Utils.hpp b/libs/utils/src/utils/encoding/Base64Utils.hpp index 6b9d88c3..36eee0d7 100644 --- a/libs/utils/src/utils/encoding/Base64Utils.hpp +++ b/libs/utils/src/utils/encoding/Base64Utils.hpp @@ -43,6 +43,13 @@ std::vector base64ToBinary(std::string_view base64); */ bool base64ToBinary(std::string_view base64, std::vector& decoded); +/** + * @brief Base64-url decode the string and store the resulting bytes in the |decoded| vector. + * Newlines are stripped from the string before decoding. Missing padding is accepted. + * Returns false if failed to decode. + */ +bool base64UrlToBinary(std::string_view base64url, std::vector& decoded); + /** * @brief Base64 decode the string and store the first 8-bytes of the result in the * returned uint64_t. If the result is more than 8-bytes then it will be truncated. @@ -57,10 +64,22 @@ uint64_t base64ToUInt64(const std::string& base64); */ std::string base64UrlToBase64(const std::string& base64url); +/** + * @brief Convert the Base64 URL string to standard Base64 in place. + * See RFC 4648 'Table 2: The "URL and Filename safe" Base 64 Alphabet' + */ +void base64UrlToBase64InPlace(std::string& base64url); + /** * @brief Convert the Base64 string to a Base64 URL string. * See RFC 4648 'Table 2: The "URL and Filename safe" Base 64 Alphabet' */ std::string base64ToBase64Url(const std::string& base64); +/** + * @brief Convert the Base64 string to a Base64 URL string in place. + * See RFC 4648 'Table 2: The "URL and Filename safe" Base 64 Alphabet' + */ +void base64ToBase64UrlInPlace(std::string& base64); + } // namespace snap::utils::encoding diff --git a/src/valdi_modules/src/valdi/coreutils/BUILD.bazel b/src/valdi_modules/src/valdi/coreutils/BUILD.bazel index fc08b9ae..1b866351 100644 --- a/src/valdi_modules/src/valdi/coreutils/BUILD.bazel +++ b/src/valdi_modules/src/valdi/coreutils/BUILD.bazel @@ -39,6 +39,7 @@ valdi_module( visibility = ["//visibility:public"], web_deps = [":coreutils_web"], web_register_native_module_id_overrides = { + "coreutils/web/Base64Native.js": "coreutils/src/Base64Native", "coreutils/web/UnicodeNative.js": "coreutils/src/UnicodeNative, coreutils/src/unicode/UnicodeNative", }, deps = [ diff --git a/src/valdi_modules/src/valdi/coreutils/src/Base64.ts b/src/valdi_modules/src/valdi/coreutils/src/Base64.ts index 83cc03de..9a2ee47a 100644 --- a/src/valdi_modules/src/valdi/coreutils/src/Base64.ts +++ b/src/valdi_modules/src/valdi/coreutils/src/Base64.ts @@ -1,55 +1,4 @@ -/** - * Ported from https://github.com/beatgammit/base64-js/blob/master/index.js - */ - -const lookup: string[] = []; -const revLookup: { [key: string]: number } = {}; - -const code = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; -for (let i = 0, len = code.length; i < len; ++i) { - lookup[i] = code[i]; - revLookup[code.charCodeAt(i)] = i; -} - -// Support decoding URL-safe base64 strings, as Node.js does. -// See: https://en.wikipedia.org/wiki/Base64#URL_applications -revLookup['-'.charCodeAt(0)] = 62; -revLookup['_'.charCodeAt(0)] = 63; - -function getLens(b64: string): [number, number] { - const len = b64.length; - - if (len % 4 > 0) { - throw new Error('Invalid string. Length must be a multiple of 4'); - } - - // Trim off extra bytes after placeholder bytes are found - // See: https://github.com/beatgammit/base64-js/issues/42 - let validLen = b64.indexOf('='); - if (validLen === -1) validLen = len; - - const placeHoldersLen = validLen === len ? 0 : 4 - (validLen % 4); - - return [validLen, placeHoldersLen]; -} - -function _byteLength(b64: string, validLen: number, placeHoldersLen: number): number { - return ((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen; -} - -function tripletToBase64(num: number): string { - return lookup[(num >> 18) & 0x3f] + lookup[(num >> 12) & 0x3f] + lookup[(num >> 6) & 0x3f] + lookup[num & 0x3f]; -} - -function encodeChunk(uint8: Uint8Array, start: number, end: number): string { - let tmp: number; - const output: string[] = []; - for (let i = start; i < end; i += 3) { - tmp = ((uint8[i] << 16) & 0xff0000) + ((uint8[i + 1] << 8) & 0xff00) + (uint8[i + 2] & 0xff); - output.push(tripletToBase64(tmp)); - } - return output.join(''); -} +import { decodeFromBase64, encodeToBase64 } from './Base64Native'; export namespace Base64 { interface Base64Options { @@ -57,91 +6,23 @@ export namespace Base64 { } export function fromByteArray(uint8: Uint8Array, { urlSafe }: Base64Options = {}): string { - let tmp: number; - const len = uint8.length; - const extraBytes = len % 3; // if we have 1 byte left, pad 2 bytes - const parts: string[] = []; - const maxChunkLength = 16383; // must be multiple of 3 - - // go through the array every three bytes, we'll deal with trailing stuff later - for (let i = 0, len2 = len - extraBytes; i < len2; i += maxChunkLength) { - parts.push(encodeChunk(uint8, i, i + maxChunkLength > len2 ? len2 : i + maxChunkLength)); - } - - // pad the end with zeros, but make sure to not forget the extra bytes - if (extraBytes === 1) { - tmp = uint8[len - 1]; - parts.push(lookup[tmp >> 2] + lookup[(tmp << 4) & 0x3f] + '=='); - } else if (extraBytes === 2) { - tmp = (uint8[len - 2] << 8) + uint8[len - 1]; - parts.push(lookup[tmp >> 10] + lookup[(tmp >> 4) & 0x3f] + lookup[(tmp << 2) & 0x3f] + '='); - } - - const base64String = parts.join(''); - - if (urlSafe) { - // Convert to URL-safe base64 - return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); - } else { - return base64String; - } + return encodeToBase64(uint8, urlSafe === true); } export function toByteArray(b64: string): Uint8Array { - let tmp = 0; - - // Add padding to make length multiple of 4, if necessary - const originalLen = b64.length; - if (originalLen % 4 > 0) { - const paddingCount = 4 - (originalLen % 4); - b64 += '='.repeat(paddingCount); - } - - const lens = getLens(b64); - const validLen = lens[0]; - const placeHoldersLen = lens[1]; - - const arr = new Uint8Array(_byteLength(b64, validLen, placeHoldersLen)); - - let curByte = 0; - - // if there are placeholders, only get up to the last complete 4 chars - const len = placeHoldersLen > 0 ? validLen - 4 : validLen; - - let i = 0; - for (i = 0; i < len; i += 4) { - tmp = - (revLookup[b64.charCodeAt(i)] << 18) | - (revLookup[b64.charCodeAt(i + 1)] << 12) | - (revLookup[b64.charCodeAt(i + 2)] << 6) | - revLookup[b64.charCodeAt(i + 3)]; - arr[curByte++] = (tmp >> 16) & 0xff; - arr[curByte++] = (tmp >> 8) & 0xff; - arr[curByte++] = tmp & 0xff; - } - - if (placeHoldersLen === 2) { - tmp = (revLookup[b64.charCodeAt(i)] << 2) | (revLookup[b64.charCodeAt(i + 1)] >> 4); - arr[curByte++] = tmp & 0xff; - } - - if (placeHoldersLen === 1) { - tmp = - (revLookup[b64.charCodeAt(i)] << 10) | - (revLookup[b64.charCodeAt(i + 1)] << 4) | - (revLookup[b64.charCodeAt(i + 2)] >> 2); - arr[curByte++] = (tmp >> 8) & 0xff; - arr[curByte++] = tmp & 0xff; - } - - return arr; + return decodeFromBase64(b64); } // base64 is 4/3 + up to two characters of the original data export function byteLength(b64: string): number { - const lens = getLens(b64); - const validLen = lens[0]; - const placeHoldersLen = lens[1]; + let validLen = b64.indexOf('='); + if (validLen === -1) validLen = b64.length; + + if (validLen % 4 === 1) { + throw new Error('Invalid string. Length must be a valid Base64 length'); + } + + const placeHoldersLen = validLen === b64.length ? (4 - (validLen % 4)) % 4 : 4 - (validLen % 4); return ((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen; } } diff --git a/src/valdi_modules/src/valdi/coreutils/src/Base64Native.d.ts b/src/valdi_modules/src/valdi/coreutils/src/Base64Native.d.ts new file mode 100644 index 00000000..525fd87f --- /dev/null +++ b/src/valdi_modules/src/valdi/coreutils/src/Base64Native.d.ts @@ -0,0 +1,3 @@ +export function encodeToBase64(uint8: Uint8Array, urlSafe: boolean): string; + +export function decodeFromBase64(base64: string): Uint8Array; diff --git a/src/valdi_modules/src/valdi/coreutils/test/Base64.spec.ts b/src/valdi_modules/src/valdi/coreutils/test/Base64.spec.ts index 020c018f..0491ecd1 100644 --- a/src/valdi_modules/src/valdi/coreutils/test/Base64.spec.ts +++ b/src/valdi_modules/src/valdi/coreutils/test/Base64.spec.ts @@ -4,22 +4,119 @@ import 'jasmine/src/jasmine'; describe('coreutils > Base64', () => { // Byte array for '<>' const byteArray = new Uint8Array([60, 60, 63, 63, 63, 62, 62]); + + function expectToThrowMessage(fn: () => void, message: string) { + let thrownMessage: string | undefined; + try { + fn(); + } catch (error) { + thrownMessage = error instanceof Error ? error.message : String(error); + } + expect(thrownMessage).toEqual(message); + } + it('should Base64 encode correctly', () => { expect(Base64.fromByteArray(byteArray)).toEqual('PDw/Pz8+Pg=='); }); + it('should url-safe Base64 encode correctly', () => { expect(Base64.fromByteArray(byteArray, { urlSafe: true })).toEqual('PDw_Pz8-Pg'); }); + it('should Base64 decode correctly with padding', () => { expect(Base64.toByteArray('PDw/Pz8+Pg==')).toEqual(byteArray); }); + it('should Base64 decode correctly without padding', () => { expect(Base64.toByteArray('PDw/Pz8+Pg')).toEqual(byteArray); }); + it('should url-safe Base64 decode correctly without padding', () => { expect(Base64.toByteArray('PDw_Pz8-Pg')).toEqual(byteArray); }); + it('should url-safe Base64 decode correctly with padding', () => { expect(Base64.toByteArray('PDw_Pz8-Pg=')).toEqual(byteArray); }); + + it('should calculate byte length correctly without padding', () => { + expect(Base64.byteLength('PDw/Pz8+Pg')).toEqual(byteArray.length); + }); + + it('should encode padding boundaries correctly', () => { + expect(Base64.fromByteArray(new Uint8Array([]))).toEqual(''); + expect(Base64.fromByteArray(new Uint8Array([102]))).toEqual('Zg=='); + expect(Base64.fromByteArray(new Uint8Array([102, 111]))).toEqual('Zm8='); + expect(Base64.fromByteArray(new Uint8Array([102, 111, 111]))).toEqual('Zm9v'); + }); + + it('should decode padding boundaries correctly', () => { + expect(Base64.toByteArray('')).toEqual(new Uint8Array([])); + expect(Base64.toByteArray('Zg==')).toEqual(new Uint8Array([102])); + expect(Base64.toByteArray('Zm8=')).toEqual(new Uint8Array([102, 111])); + expect(Base64.toByteArray('Zm9v')).toEqual(new Uint8Array([102, 111, 111])); + }); + + it('should calculate byte length for padded and unpadded boundaries', () => { + expect(Base64.byteLength('')).toEqual(0); + expect(Base64.byteLength('Zg==')).toEqual(1); + expect(Base64.byteLength('Zg')).toEqual(1); + expect(Base64.byteLength('Zm8=')).toEqual(2); + expect(Base64.byteLength('Zm8')).toEqual(2); + expect(Base64.byteLength('Zm9v')).toEqual(3); + expect(Base64.byteLength('Zm9vYg')).toEqual(4); + expect(Base64.byteLength('Zm9vYmE')).toEqual(5); + expect(Base64.byteLength('Zm9vYmFy')).toEqual(6); + }); + + it('should calculate byte length for URL-safe strings', () => { + expect(Base64.byteLength('_P-_')).toEqual(3); + expect(Base64.byteLength('PDw_Pz8-Pg')).toEqual(byteArray.length); + }); + + it('should round trip arbitrary binary bytes', () => { + const bytes = new Uint8Array([0, 1, 2, 3, 250, 251, 252, 253, 254, 255]); + expect(Base64.toByteArray(Base64.fromByteArray(bytes))).toEqual(bytes); + }); + + it('should encode and decode URL-safe binary bytes', () => { + const bytes = new Uint8Array([252, 255, 191]); + const encoded = Base64.fromByteArray(bytes, { urlSafe: true }); + expect(encoded).toEqual('_P-_'); + expect(Base64.toByteArray(encoded)).toEqual(bytes); + expect(Base64.byteLength(encoded)).toEqual(bytes.length); + }); + + it('should decode Base64 with newlines', () => { + expect(Base64.toByteArray('PDw/\nPz8+\r\nPg==')).toEqual(byteArray); + expect(Base64.toByteArray('PDw_\nPz8-\r\nPg')).toEqual(byteArray); + }); + + it('should encode standard characters that become URL-safe replacements', () => { + const bytes = new Uint8Array([252, 255, 191]); + expect(Base64.fromByteArray(bytes)).toEqual('/P+/'); + expect(Base64.fromByteArray(bytes, { urlSafe: true })).toEqual('_P-_'); + }); + + it('should round trip a larger payload', () => { + const bytes = new Uint8Array(100000); + for (let i = 0; i < bytes.length; i++) { + bytes[i] = (i * 31 + 17) & 0xff; + } + + const encoded = Base64.fromByteArray(bytes); + expect(Base64.byteLength(encoded)).toEqual(bytes.length); + expect(Base64.toByteArray(encoded)).toEqual(bytes); + + const urlSafeEncoded = Base64.fromByteArray(bytes, { urlSafe: true }); + expect(Base64.byteLength(urlSafeEncoded)).toEqual(bytes.length); + expect(Base64.toByteArray(urlSafeEncoded)).toEqual(bytes); + }); + + it('should reject invalid Base64', () => { + expectToThrowMessage(() => Base64.toByteArray('Z'), 'Invalid base64 string'); + expectToThrowMessage(() => Base64.toByteArray('Zg!'), 'Invalid base64 string'); + expectToThrowMessage(() => Base64.toByteArray('Z=g='), 'Invalid base64 string'); + expectToThrowMessage(() => Base64.byteLength('Z'), 'Invalid string. Length must be a valid Base64 length'); + }); }); diff --git a/src/valdi_modules/src/valdi/coreutils/web/Base64Native.ts b/src/valdi_modules/src/valdi/coreutils/web/Base64Native.ts new file mode 100644 index 00000000..5c27dea6 --- /dev/null +++ b/src/valdi_modules/src/valdi/coreutils/web/Base64Native.ts @@ -0,0 +1,49 @@ +const chunkSize = 0x8000; + +function bytesToBinaryString(uint8: Uint8Array): string { + const parts: string[] = []; + + for (let i = 0; i < uint8.length; i += chunkSize) { + const chunk = uint8.subarray(i, i + chunkSize); + let part = ''; + for (let j = 0; j < chunk.length; j++) { + part += String.fromCharCode(chunk[j]); + } + parts.push(part); + } + + return parts.join(''); +} + +function binaryStringToBytes(binary: string): Uint8Array { + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytes; +} + +function normalizeBase64Input(base64: string): string { + const standardBase64 = base64.replace(/-/g, '+').replace(/_/g, '/'); + const missingPadding = standardBase64.length % 4; + + if (missingPadding === 0) { + return standardBase64; + } + + return standardBase64 + '='.repeat(4 - missingPadding); +} + +export function encodeToBase64(uint8: Uint8Array, urlSafe: boolean): string { + const base64String = btoa(bytesToBinaryString(uint8)); + + if (urlSafe) { + return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); + } + + return base64String; +} + +export function decodeFromBase64(base64: string): Uint8Array { + return binaryStringToBytes(atob(normalizeBase64Input(base64))); +} diff --git a/src/valdi_modules/src/valdi/file_system/src/FileSystemModule.d.ts b/src/valdi_modules/src/valdi/file_system/src/FileSystemModule.d.ts index 158a8984..107cdab7 100644 --- a/src/valdi_modules/src/valdi/file_system/src/FileSystemModule.d.ts +++ b/src/valdi_modules/src/valdi/file_system/src/FileSystemModule.d.ts @@ -4,6 +4,8 @@ export interface ReadFileOptions { encoding?: FileEncoding | undefined | null; } +export type WriteFileData = ArrayBuffer | Uint8Array | string; + /** * Valdi File System module * This FS API right now is only for internal usage due to some limitations. @@ -17,7 +19,7 @@ export interface FileSystemModule { readFileSync(path: string, options?: ReadFileOptions): string | ArrayBuffer; - writeFileSync(path: string, data: ArrayBuffer | string): void; + writeFileSync(path: string, data: WriteFileData): void; currentWorkingDirectory(): string; -} \ No newline at end of file +} diff --git a/src/valdi_modules/src/valdi/file_system/test/FileSystem.spec.ts b/src/valdi_modules/src/valdi/file_system/test/FileSystem.spec.ts index b13e76b5..48f5c76d 100644 --- a/src/valdi_modules/src/valdi/file_system/test/FileSystem.spec.ts +++ b/src/valdi_modules/src/valdi/file_system/test/FileSystem.spec.ts @@ -47,6 +47,21 @@ describe('File System Module', () => { expect(resultFromFileRemove).toBeTrue(); }); + it('should write Uint8Array content', () => { + const fileName = `${VALDI_MODULES_ROOT}/file_system/test/write_uint8_array_test_file.txt`; + const source = new Uint8Array([120, 116, 101, 115, 116, 32, 100, 97, 116, 97, 121]); + const fileContent = source.subarray(1, source.length - 1); + + fs.writeFileSync(fileName, fileContent); + try { + const result = fs.readFileSync(fileName, { encoding: 'utf8' }); + + expect(result).toEqual('test data'); + } finally { + fs.removeSync(fileName); + } + }); + it('should get current root directory for client repository', () => { expect(() => fs.currentWorkingDirectory()).not.toThrow(); }); diff --git a/src/valdi_modules/src/valdi/valdi_core/src/LazyImport.ts b/src/valdi_modules/src/valdi/valdi_core/src/LazyImport.ts new file mode 100644 index 00000000..9d3bbf9a --- /dev/null +++ b/src/valdi_modules/src/valdi/valdi_core/src/LazyImport.ts @@ -0,0 +1,39 @@ +import { RequireFunc } from './IModuleLoader'; + +declare global { + const require: RequireFunc; +} + +export interface LazyImport { + readonly get: TModule; +} + +class LazyImportImpl implements LazyImport { + private didLoad = false; + private module: TModule | undefined; + + constructor(private readonly requireFunc: RequireFunc, private readonly path: string) {} + + get get(): TModule { + if (!this.didLoad) { + this.module = this.requireFunc(this.path, true) as TModule; + this.didLoad = true; + } + + return this.module as TModule; + } +} + +/* + * Creates a synchronous, cached lazy module reference. + * + * Pass the current module's `require` so relative paths resolve from the call + * site, and use a type-only import to keep the result strongly typed without + * emitting an eager runtime import: + * + * const Symbolicator = lazyImport(require, '../Symbolicator'); + * Symbolicator.get.symbolicate(error); + */ +export function lazyImport(requireFunc: RequireFunc, path: string): LazyImport { + return new LazyImportImpl(requireFunc, path); +} diff --git a/src/valdi_modules/src/valdi/valdi_core/src/utils/StringUtils.ts b/src/valdi_modules/src/valdi/valdi_core/src/utils/StringUtils.ts index 8e8531c5..d68fcf90 100644 --- a/src/valdi_modules/src/valdi/valdi_core/src/utils/StringUtils.ts +++ b/src/valdi_modules/src/valdi/valdi_core/src/utils/StringUtils.ts @@ -1,4 +1,6 @@ -import { symbolicate } from '../Symbolicator'; +import { lazyImport } from '../LazyImport'; + +const Symbolicator = lazyImport(require, '../Symbolicator'); export function toCamelCase(snakeCaseString: string): string { const components = snakeCaseString.split('_'); @@ -109,7 +111,7 @@ function stringifyInner( return ``; } if (object instanceof Error) { - const symbolicatedError = symbolicate(object); + const symbolicatedError = Symbolicator.get.symbolicate(object); let errorString = `${symbolicatedError.name}:${symbolicatedError.message}`; if (symbolicatedError.stack) { diff --git a/valdi/BUILD.bazel b/valdi/BUILD.bazel index 92f2daf7..e6edbdf2 100644 --- a/valdi/BUILD.bazel +++ b/valdi/BUILD.bazel @@ -855,6 +855,7 @@ valdi_test( "//src/valdi_modules/src/valdi/persistence:persistence_native", "//src/valdi_modules/src/valdi/valdi_core:valdi_core_native", "//src/valdi_modules/src/valdi/valdi_protobuf:valdi_protobuf_native", + "//src/valdi_modules/src/valdi/valdi_standalone:valdi_standalone_native", "//tsn", "//valdi/testdata/resources/modules/local:local_native", "//valdi/testdata/resources/modules/remote:remote_native", diff --git a/valdi/src/valdi/hermes/HermesJavaScriptContext.cpp b/valdi/src/valdi/hermes/HermesJavaScriptContext.cpp index 2a01cf3a..9bc09374 100644 --- a/valdi/src/valdi/hermes/HermesJavaScriptContext.cpp +++ b/valdi/src/valdi/hermes/HermesJavaScriptContext.cpp @@ -12,8 +12,8 @@ #include "valdi/hermes/HermesUtils.hpp" #include "valdi/runtime/JavaScript/JavaScriptFunctionCallContext.hpp" +#include "valdi/runtime/JavaScript/JSNativeClassData.hpp" #include "valdi/runtime/JavaScript/JavaScriptUtils.hpp" -#include "valdi/runtime/Utils/RefCountableAutoreleasePool.hpp" #include "valdi_core/cpp/Text/UTF16Utils.hpp" #include "valdi_core/cpp/Utils/ReferenceInfo.hpp" @@ -21,6 +21,9 @@ #include "valdi_core/cpp/Utils/StringCache.hpp" #include "valdi_core/cpp/Utils/ValueUtils.hpp" +#include "hermes/VM/NativeState.h" +#include "hermes/VM/PropertyAccessor.h" + #ifdef HERMES_ENABLE_DEBUGGER #include "valdi/hermes/HermesDebuggerServer.hpp" #endif @@ -57,16 +60,6 @@ class UnhandledPromiseCallback : public JSFunctionBase { } }; -static void freeNativeState(hermes::vm::GC& /*gc*/, hermes::vm::NativeState* ns) { - if (ns != nullptr) { - Valdi::RefCountableAutoreleasePool::release(ns->context()); - } -} - -static void freeContext(void* context) { - Valdi::RefCountableAutoreleasePool::release(context); -} - static hermes::vm::RuntimeConfig makeRuntimeConfig() { return hermes::vm::RuntimeConfig::Builder() // 256kb stack size to be consistent with quickjs - Hermes's default is 128kb @@ -550,6 +543,224 @@ JSValueRef HermesJavaScriptContext::newWrappedObject(const Ref& wr return toJSValueRefUncached(result.getHermesValue()); } +static hermes::vm::DefinePropertyFlags toHermesDataPropertyFlags(bool writable, bool enumerable, bool configurable) { + hermes::vm::DefinePropertyFlags flags; + flags.setEnumerable = 1; + flags.enumerable = enumerable; + flags.setWritable = 1; + flags.writable = writable; + flags.setConfigurable = 1; + flags.configurable = configurable; + flags.setValue = 1; + return flags; +} + +static hermes::vm::DefinePropertyFlags toHermesAccessorPropertyFlags(bool hasGetter, + bool hasSetter, + bool enumerable, + bool configurable) { + hermes::vm::DefinePropertyFlags flags; + flags.setEnumerable = 1; + flags.enumerable = enumerable; + flags.setConfigurable = 1; + flags.configurable = configurable; + flags.setGetter = hasGetter; + flags.setSetter = hasSetter; + return flags; +} + +JSValueRef HermesJavaScriptContext::newNativeClass(const Ref& classOpaque, + const JSClassDefinition& classDefinition, + JSExceptionTracker& exceptionTracker) { + hermes::vm::GCScope gcScope(*_runtime); + + auto nativeClass = + makeShared(classDefinition.getName(), classOpaque, classDefinition.getConstructor()); + auto backendClass = makeShared(*this, nativeClass); + auto prototype = _runtime->makeHandle(hermes::vm::JSObject::create(*_runtime)); + auto constructor = _runtime->makeHandle(hermes::vm::NativeConstructor::create( + *_runtime, + hermes::vm::Handle::vmcast(&_runtime->functionPrototype), + backendClass.get(), + &callNativeClassConstructor, + 0, + &createNativeClassObject, + hermes::vm::CellKind::JSObjectKind)); + + auto className = newSymbolIDFromString(classDefinition.getName().toStringView()); + auto defineConstructorStatus = hermes::vm::Callable::defineNameLengthAndPrototype( + constructor, *_runtime, className.get(), 0, prototype, hermes::vm::Callable::WritablePrototype::No, false); + if (!checkException(defineConstructorStatus, exceptionTracker)) { + return newUndefined(); + } + if (!checkException(setNativeState(*_runtime, constructor, backendClass), exceptionTracker)) { + return newUndefined(); + } + + auto createFunction = [&](const Ref& functionData, + auto call, + std::string_view name) -> hermes::vm::Handle { + auto functionName = newSymbolIDFromString(name); + auto result = + hermes::vm::FinalizableNativeFunction::createWithoutPrototype(*_runtime, + unsafeBridgeRetain(functionData.get()), + call, + &freeContext, + functionName.get(), + 0); + if (!checkException(result.getStatus(), exceptionTracker)) { + return hermes::vm::Runtime::makeNullHandle(); + } + return _runtime->makeHandle(result.getValue()); + }; + + auto defineDataProperty = [&](hermes::vm::Handle object, + std::string_view name, + hermes::vm::Handle<> value, + bool writable, + bool enumerable, + bool configurable) -> bool { + auto propertyName = newSymbolIDFromString(name); + auto result = + hermes::vm::JSObject::defineOwnProperty(object, + *_runtime, + propertyName.get(), + toHermesDataPropertyFlags(writable, enumerable, configurable), + value, + hermes::vm::PropOpFlags().plusThrowOnError()); + return checkException(result.getStatus(), exceptionTracker) && result.getValue(); + }; + + auto defineAccessorProperty = [&](hermes::vm::Handle object, + std::string_view name, + hermes::vm::Handle getter, + hermes::vm::Handle setter, + bool enumerable, + bool configurable) -> bool { + auto propertyName = newSymbolIDFromString(name); + auto accessor = _runtime->makeHandle( + hermes::vm::PropertyAccessor::create(*_runtime, getter, setter)); + auto result = hermes::vm::JSObject::defineOwnProperty( + object, + *_runtime, + propertyName.get(), + toHermesAccessorPropertyFlags( + static_cast(getter), static_cast(setter), enumerable, configurable), + accessor, + hermes::vm::PropOpFlags().plusThrowOnError()); + return checkException(result.getStatus(), exceptionTracker) && result.getValue(); + }; + + auto constructorObject = hermes::vm::Handle::vmcast(constructor); + for (const auto& entry : classDefinition.getEntries()) { + auto object = entry.isClassMember() ? constructorObject : prototype; + auto memberCallback = + entry.isClassMember() ? &callNativeClassStaticMember : &callNativeClassInstanceMember; + switch (entry.getKind()) { + case JSClassEntryKind::Method: { + if (entry.getMethodCallback() == nullptr) { + exceptionTracker.onError("Native class method callback cannot be null"); + return newUndefined(); + } + const auto& propertyName = entry.getName(); + auto functionData = makeShared(*this, nativeClass, propertyName); + functionData->callback = entry.getMethodCallback(); + auto function = createFunction(functionData, memberCallback, propertyName.toStringView()); + if (!exceptionTracker || !defineDataProperty(object, + propertyName.toStringView(), + function, + entry.isWritable(), + entry.isEnumerable(), + entry.isConfigurable())) { + return newUndefined(); + } + break; + } + case JSClassEntryKind::Constant: + if (!defineDataProperty(object, + entry.getName().toStringView(), + toHandle(entry.getValue().get()), + entry.isWritable(), + entry.isEnumerable(), + entry.isConfigurable())) { + return newUndefined(); + } + break; + case JSClassEntryKind::Accessor: { + if (entry.getGetterCallback() == nullptr && entry.getSetterCallback() == nullptr) { + exceptionTracker.onError("Native class accessor must have a getter or setter"); + return newUndefined(); + } + auto getter = hermes::vm::Runtime::makeNullHandle(); + auto setter = hermes::vm::Runtime::makeNullHandle(); + const auto& propertyName = entry.getName(); + if (entry.getGetterCallback() != nullptr) { + auto functionData = makeShared(*this, nativeClass, propertyName); + functionData->callback = entry.getGetterCallback(); + getter = createFunction(functionData, memberCallback, propertyName.toStringView()); + } + if (exceptionTracker && entry.getSetterCallback() != nullptr) { + auto functionData = makeShared(*this, nativeClass, propertyName); + functionData->callback = entry.getSetterCallback(); + setter = createFunction(functionData, memberCallback, propertyName.toStringView()); + } + if (!exceptionTracker || + !defineAccessorProperty(object, + propertyName.toStringView(), + getter, + setter, + entry.isEnumerable(), + entry.isConfigurable())) { + return newUndefined(); + } + break; + } + } + } + + return toJSValueRefUncached(constructor.getHermesValue()); +} + +JSValueRef HermesJavaScriptContext::newObjectFromNativeClass(const Ref& opaque, + const JSValue& cls, + JSExceptionTracker& exceptionTracker) { + hermes::vm::GCScope gcScope(*_runtime); + auto clsObject = toJSObject(cls, exceptionTracker); + if (!exceptionTracker) { + return newUndefined(); + } + auto backendClass = Ref(dynamic_cast(getOwnNativeStateContext(*_runtime, clsObject))); + if (backendClass == nullptr) { + exceptionTracker.onError("Value is not a native class"); + return newUndefined(); + } + if (opaque == nullptr) { + exceptionTracker.onError("Native class opaque object cannot be null"); + return newUndefined(); + } + + auto prototypeResult = hermes::vm::JSObject::getNamedOrIndexed( + clsObject, + *_runtime, + hermes::vm::Predefined::getSymbolID(hermes::vm::Predefined::prototype)); + if (!checkException(prototypeResult.getStatus(), exceptionTracker)) { + return newUndefined(); + } + auto prototypeValue = prototypeResult.getValue().getHermesValue(); + if (!prototypeValue.isObject()) { + exceptionTracker.onError("Native class prototype is not an object"); + return newUndefined(); + } + auto prototype = _runtime->makeHandle(prototypeValue); + + auto object = _runtime->makeHandle(hermes::vm::JSObject::create(*_runtime, prototype)); + auto instanceData = makeShared(backendClass->nativeClass, opaque); + if (!checkException(setNativeState(*_runtime, object, instanceData), exceptionTracker)) { + return newUndefined(); + } + return toJSValueRefUncached(object.getHermesValue()); +} + JSValueRef HermesJavaScriptContext::getObjectProperty(const JSValue& object, const JSPropertyName& propertyName, JSExceptionTracker& exceptionTracker) { @@ -923,7 +1134,7 @@ Ref HermesJavaScriptContext::valueToWrappedObject(const JSValue& v auto* nativeState = hermes::vm::vmcast(result.getValue().get()); - return unsafeBridge(nativeState->context()); + return unwrapNativeClassInstanceData(unsafeBridge(nativeState->context())); } Ref HermesJavaScriptContext::valueToFunction(const JSValue& value, JSExceptionTracker& exceptionTracker) { @@ -933,8 +1144,12 @@ Ref HermesJavaScriptContext::valueToFunction(const JSValue& value, J return nullptr; } - auto* functionData = unsafeBridgeUnretained( + auto* functionContext = unsafeBridgeUnretained( hermes::vm::vmcast(*hermesValue)->getContext()); + auto* functionData = dynamic_cast(functionContext); + if (functionData == nullptr) { + return nullptr; + } return functionData->function; } diff --git a/valdi/src/valdi/hermes/HermesJavaScriptContext.hpp b/valdi/src/valdi/hermes/HermesJavaScriptContext.hpp index 3c657721..9362e68a 100644 --- a/valdi/src/valdi/hermes/HermesJavaScriptContext.hpp +++ b/valdi/src/valdi/hermes/HermesJavaScriptContext.hpp @@ -65,6 +65,14 @@ class HermesJavaScriptContext : public IJavaScriptContext { JSValueRef newWrappedObject(const Ref& wrappedObject, JSExceptionTracker& exceptionTracker) final; + JSValueRef newNativeClass(const Ref& classOpaque, + const JSClassDefinition& classDefinition, + JSExceptionTracker& exceptionTracker) final; + + JSValueRef newObjectFromNativeClass(const Ref& opaque, + const JSValue& cls, + JSExceptionTracker& exceptionTracker) final; + JSValueRef newWeakRef(const JSValue& object, JSExceptionTracker& exceptionTracker) final; JSValueRef getObjectProperty(const JSValue& object, diff --git a/valdi/src/valdi/hermes/HermesUtils.cpp b/valdi/src/valdi/hermes/HermesUtils.cpp index 85e69c48..dffbeb8a 100644 --- a/valdi/src/valdi/hermes/HermesUtils.cpp +++ b/valdi/src/valdi/hermes/HermesUtils.cpp @@ -8,14 +8,78 @@ #include "valdi/hermes/HermesUtils.hpp" #include "valdi/hermes/HermesJavaScriptContext.hpp" #include "valdi/runtime/JavaScript/JavaScriptFunctionCallContext.hpp" +#include "valdi/runtime/Utils/RefCountableAutoreleasePool.hpp" #include "valdi_core/cpp/Constants.hpp" +#include "hermes/VM/NativeState.h" + namespace Valdi::Hermes { JSFunctionData::JSFunctionData(HermesJavaScriptContext& jsContext, const Valdi::Ref& function) : jsContext(jsContext), function(function) {} JSFunctionData::~JSFunctionData() = default; +HermesNativeClassData::HermesNativeClassData(HermesJavaScriptContext& jsContext, + const Ref& nativeClass) + : jsContext(jsContext), nativeClass(nativeClass) {} + +HermesNativeClassData::~HermesNativeClassData() = default; + +HermesNativeClassFunctionData::HermesNativeClassFunctionData(HermesJavaScriptContext& jsContext, + const Ref& nativeClass, + const StringBox& name) + : jsContext(jsContext), + nativeClass(nativeClass), + referenceInfo(nativeClass->makeMemberReferenceInfo(name)) {} + +HermesNativeClassFunctionData::~HermesNativeClassFunctionData() = default; + +void freeNativeState(hermes::vm::GC& /*gc*/, hermes::vm::NativeState* nativeState) { + if (nativeState != nullptr) { + RefCountableAutoreleasePool::release(nativeState->context()); + } +} + +void freeContext(void* context) { + RefCountableAutoreleasePool::release(context); +} + +RefCountable* getOwnNativeStateContext(hermes::vm::Runtime& runtime, + hermes::vm::Handle object) { + hermes::vm::NamedPropertyDescriptor descriptor; + if (!hermes::vm::JSObject::getOwnNamedDescriptor( + object, + runtime, + hermes::vm::Predefined::getSymbolID(hermes::vm::Predefined::InternalPropertyNativeState), + descriptor)) { + return nullptr; + } + + auto value = hermes::vm::JSObject::getNamedSlotValueUnsafe(*object, runtime, descriptor); + if (!value.isObject()) { + return nullptr; + } + auto* objectValue = value.getObject(runtime); + if (!hermes::vm::vmisa(objectValue)) { + return nullptr; + } + return reinterpret_cast(hermes::vm::vmcast(objectValue)->context()); +} + +hermes::vm::ExecutionStatus setNativeState(hermes::vm::Runtime& runtime, + hermes::vm::Handle object, + const Ref& data) { + auto nativeState = + runtime.makeHandle(hermes::vm::NativeState::create(runtime, unsafeBridgeRetain(data.get()), &freeNativeState)); + auto result = hermes::vm::JSObject::defineOwnProperty( + object, + runtime, + hermes::vm::Predefined::getSymbolID(hermes::vm::Predefined::InternalPropertyNativeState), + hermes::vm::DefinePropertyFlags::getNewNonEnumerableFlags(), + nativeState); + return result.getStatus(); +} + static hermes::vm::ExecutionStatus onJsCallError(hermes::vm::Runtime& runtime, Valdi::JSExceptionTracker& exceptionTracker) { auto exception = exceptionTracker.getExceptionAndClear(); @@ -54,6 +118,138 @@ hermes::vm::CallResult callTrampoline(void* context, } } +hermes::vm::CallResult callNativeClassInstanceMember( + void* context, hermes::vm::Runtime& runtime, hermes::vm::NativeArgs args) { + auto functionData = unsafeBridge(context); + auto& jsContext = functionData->jsContext; + + auto argumentCount = args.getArgCount(); + JSValueRef arguments[argumentCount]; + for (size_t i = 0; i < argumentCount; i++) { + arguments[i] = jsContext.toJSValueRef(args.getArg(i)); + } + + JSExceptionTracker exceptionTracker(jsContext); + JSFunctionNativeCallContext callContext(jsContext, + argumentCount == 0 ? nullptr : arguments, + argumentCount, + exceptionTracker, + functionData->referenceInfo); + auto thisValue = jsContext.toJSValueRef(args.getThisArg()); + callContext.setThisValue(thisValue.get()); + + auto thisObject = args.dyncastThis(); + auto instanceData = + thisObject ? Ref(dynamic_cast(getOwnNativeStateContext(runtime, thisObject))) : + Ref(); + if (instanceData == nullptr || instanceData->getNativeClass() != functionData->nativeClass) { + return runtime.raiseTypeError("Native class member called with an incompatible receiver"); + } + + if (jsContext.interruptRequested()) { + jsContext.onInterrupt(); + } + + auto result = functionData->callback(instanceData->getOpaque().get(), callContext); + if (!exceptionTracker) { + return onJsCallError(runtime, exceptionTracker); + } + return HermesJavaScriptContext::toHermesValue(result.get()); +} + +hermes::vm::CallResult callNativeClassStaticMember( + void* context, hermes::vm::Runtime& runtime, hermes::vm::NativeArgs args) { + auto functionData = unsafeBridge(context); + auto& jsContext = functionData->jsContext; + + auto argumentCount = args.getArgCount(); + JSValueRef arguments[argumentCount]; + for (size_t i = 0; i < argumentCount; i++) { + arguments[i] = jsContext.toJSValueRef(args.getArg(i)); + } + + JSExceptionTracker exceptionTracker(jsContext); + JSFunctionNativeCallContext callContext(jsContext, + argumentCount == 0 ? nullptr : arguments, + argumentCount, + exceptionTracker, + functionData->referenceInfo); + auto thisValue = jsContext.toJSValueRef(args.getThisArg()); + callContext.setThisValue(thisValue.get()); + + if (jsContext.interruptRequested()) { + jsContext.onInterrupt(); + } + + auto result = functionData->callback(functionData->nativeClass->getOpaque().get(), callContext); + if (!exceptionTracker) { + return onJsCallError(runtime, exceptionTracker); + } + return HermesJavaScriptContext::toHermesValue(result.get()); +} + +hermes::vm::CallResult> createNativeClassObject( + hermes::vm::Runtime& runtime, hermes::vm::Handle prototype, void* /*context*/) { + return hermes::vm::JSObject::create(runtime, prototype); +} + +hermes::vm::CallResult callNativeClassConstructor(void* context, + hermes::vm::Runtime& runtime, + hermes::vm::NativeArgs args) { + auto classData = unsafeBridge(context); + if (!args.isConstructorCall()) { + return runtime.raiseTypeError("Native class constructor must be called with new"); + } + + auto newTargetValue = args.getNewTargetHandle(); + auto newTargetObject = hermes::vm::vmisa(newTargetValue.get()) ? + hermes::vm::Handle::vmcast(newTargetValue) : + hermes::vm::Runtime::makeNullHandle(); + auto* newTargetClassData = + newTargetObject ? dynamic_cast(getOwnNativeStateContext(runtime, newTargetObject)) : + nullptr; + if (newTargetClassData != classData.get()) { + return runtime.raiseTypeError("Native class subclassing is not supported"); + } + auto constructor = classData->nativeClass->getConstructor(); + if (constructor == nullptr) { + return runtime.raiseTypeError("Native class cannot be constructed from JavaScript"); + } + + auto& jsContext = classData->jsContext; + auto argumentCount = args.getArgCount(); + JSValueRef arguments[argumentCount]; + for (size_t i = 0; i < argumentCount; i++) { + arguments[i] = jsContext.toJSValueRef(args.getArg(i)); + } + + JSExceptionTracker exceptionTracker(jsContext); + const auto& referenceInfo = classData->nativeClass->getConstructorReferenceInfo(); + JSFunctionNativeCallContext callContext( + jsContext, argumentCount == 0 ? nullptr : arguments, argumentCount, exceptionTracker, referenceInfo); + auto thisValue = jsContext.toJSValueRef(args.getThisArg()); + callContext.setThisValue(thisValue.get()); + + if (jsContext.interruptRequested()) { + jsContext.onInterrupt(); + } + + auto nativeOpaque = constructor(classData->nativeClass->getOpaque().get(), callContext); + if (exceptionTracker && nativeOpaque == nullptr) { + exceptionTracker.onError("Native class constructor returned a null opaque object"); + } + if (!exceptionTracker) { + return onJsCallError(runtime, exceptionTracker); + } + + auto thisObject = args.dyncastThis(); + auto instanceData = makeShared(classData->nativeClass, nativeOpaque); + if (setNativeState(runtime, thisObject, instanceData) == hermes::vm::ExecutionStatus::EXCEPTION) { + return hermes::vm::ExecutionStatus::EXCEPTION; + } + return args.getThisArg(); +} + void ByteBufferOStream::write_impl(const char* Ptr, size_t Size) { _output.append(Ptr, Ptr + Size); } diff --git a/valdi/src/valdi/hermes/HermesUtils.hpp b/valdi/src/valdi/hermes/HermesUtils.hpp index d8d1a5bb..057ae659 100644 --- a/valdi/src/valdi/hermes/HermesUtils.hpp +++ b/valdi/src/valdi/hermes/HermesUtils.hpp @@ -9,6 +9,7 @@ #include "valdi/hermes/Hermes.hpp" #include "valdi/runtime/Interfaces/IJavaScriptContext.hpp" +#include "valdi/runtime/JavaScript/JSNativeClassData.hpp" #include "valdi_core/cpp/Utils/ByteBuffer.hpp" namespace Valdi::Hermes { @@ -86,10 +87,52 @@ struct JSFunctionData : public Valdi::SimpleRefCountable { ~JSFunctionData() override; }; +struct HermesNativeClassData final : public SimpleRefCountable { + HermesNativeClassData(HermesJavaScriptContext& jsContext, const Ref& nativeClass); + ~HermesNativeClassData() override; + + HermesJavaScriptContext& jsContext; + Ref nativeClass; +}; + +struct HermesNativeClassFunctionData final : public SimpleRefCountable { + HermesNativeClassFunctionData(HermesJavaScriptContext& jsContext, + const Ref& nativeClass, + const StringBox& name); + ~HermesNativeClassFunctionData() override; + + HermesJavaScriptContext& jsContext; + Ref nativeClass; + ReferenceInfo referenceInfo; + JSClassCallback callback = nullptr; +}; + +void freeNativeState(hermes::vm::GC& gc, hermes::vm::NativeState* nativeState); +void freeContext(void* context); + +RefCountable* getOwnNativeStateContext(hermes::vm::Runtime& runtime, + hermes::vm::Handle object); + +hermes::vm::ExecutionStatus setNativeState(hermes::vm::Runtime& runtime, + hermes::vm::Handle object, + const Ref& data); + hermes::vm::CallResult callTrampoline(void* context, hermes::vm::Runtime& runtime, hermes::vm::NativeArgs args); +hermes::vm::CallResult callNativeClassInstanceMember( + void* context, hermes::vm::Runtime& runtime, hermes::vm::NativeArgs args); + +hermes::vm::CallResult callNativeClassStaticMember( + void* context, hermes::vm::Runtime& runtime, hermes::vm::NativeArgs args); + +hermes::vm::CallResult callNativeClassConstructor( + void* context, hermes::vm::Runtime& runtime, hermes::vm::NativeArgs args); + +hermes::vm::CallResult> createNativeClassObject( + hermes::vm::Runtime& runtime, hermes::vm::Handle prototype, void* context); + class ByteBufferOStream : public llvh::raw_ostream { ByteBuffer& _output; diff --git a/valdi/src/valdi/jscore/JSCoreCustomClasses.cpp b/valdi/src/valdi/jscore/JSCoreCustomClasses.cpp index 9f84a8d3..b50cac32 100644 --- a/valdi/src/valdi/jscore/JSCoreCustomClasses.cpp +++ b/valdi/src/valdi/jscore/JSCoreCustomClasses.cpp @@ -24,6 +24,12 @@ static JSValueRef onJsCallError(JSContextRef ctx, return JSValueMakeUndefined(ctx); } +static JSObjectRef onJsConstructorError(Valdi::JSExceptionTracker& exceptionTracker, JSValueRef* exceptionPtr) { + auto exception = exceptionTracker.getExceptionAndClear(); + *exceptionPtr = fromValdiJSValue(exception.get()).valueRef; + return nullptr; +} + JSValueRef callAsFunction(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, @@ -60,6 +66,208 @@ JSValueRef callAsFunction(JSContextRef ctx, } } +static JSValueRef callNativeClassAsFunction(JSContextRef ctx, + JSObjectRef function, + JSObjectRef /*thisObject*/, + size_t /*argumentCount*/, + const JSValueRef[] /*arguments*/, + JSValueRef* exception) { + auto functionData = Valdi::Ref(getAttachedNativeClassFunctionData(function)); + auto& jsContext = functionData->jsContext; + Valdi::JSExceptionTracker exceptionTracker(jsContext); + exceptionTracker.onError("Native class constructor must be called with new"); + return onJsCallError(ctx, exceptionTracker, exception); +} + +static JSObjectRef callNativeClassConstructor(JSContextRef ctx, + JSObjectRef constructorObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + auto functionData = Valdi::Ref(getAttachedNativeClassFunctionData(constructorObject)); + auto& jsContext = functionData->jsContext; + Valdi::JSExceptionTracker exceptionTracker(jsContext); + + Valdi::JSValueRef outArguments[argumentCount]; + for (size_t i = 0; i < argumentCount; i++) { + outArguments[i] = Valdi::JSValueRef::makeUnretained( + jsContext, toValdiJSValue(JSCoreRef(arguments[i], JSValueGetType(ctx, arguments[i])))); + } + + JSValueRef jsException = nullptr; + auto prototypeValue = JSObjectGetProperty( + ctx, constructorObject, fromValdiJSPropertyName(jsContext.getPrototypePropertyName()), &jsException); + if (jsException != nullptr) { + exceptionTracker.storeException(Valdi::JSValueRef::makeRetained( + jsContext, toValdiJSValue(JSCoreRef(jsException, JSValueGetType(ctx, jsException))))); + return onJsConstructorError(exceptionTracker, exception); + } + auto prototypeObject = JSValueToObject(ctx, prototypeValue, &jsException); + if (jsException != nullptr) { + exceptionTracker.storeException(Valdi::JSValueRef::makeRetained( + jsContext, toValdiJSValue(JSCoreRef(jsException, JSValueGetType(ctx, jsException))))); + return onJsConstructorError(exceptionTracker, exception); + } + + auto object = JSObjectMake(ctx, getWrappedObjectClassRef(), nullptr); + JSObjectSetPrototype(ctx, object, prototypeObject); + + Valdi::JSFunctionNativeCallContext callContext(jsContext, + argumentCount == 0 ? nullptr : outArguments, + argumentCount, + exceptionTracker, + functionData->referenceInfo); + callContext.setThisValue(toValdiJSValue(JSCoreRef(object, kJSTypeObject))); + + auto constructor = functionData->nativeClass->getConstructor(); + if (constructor == nullptr) { + auto error = + jsContext.newError("Native class cannot be constructed from JavaScript", std::nullopt, exceptionTracker); + if (exceptionTracker) { + exceptionTracker.storeException(std::move(error)); + } + return onJsConstructorError(exceptionTracker, exception); + } + if (jsContext.interruptRequested()) { + jsContext.onInterrupt(); + } + + auto nativeOpaque = constructor(functionData->nativeClass->getOpaque().get(), callContext); + if (exceptionTracker && nativeOpaque == nullptr) { + exceptionTracker.onError("Native class constructor returned a null opaque object"); + } + if (!exceptionTracker) { + return onJsConstructorError(exceptionTracker, exception); + } + + auto instanceData = Valdi::makeShared(functionData->nativeClass, nativeOpaque); + auto* retainedInstanceData = Valdi::unsafeBridgeRetain(instanceData.get()); + if (!JSObjectSetPrivate(object, retainedInstanceData)) { + Valdi::RefCountableAutoreleasePool::release(retainedInstanceData); + exceptionTracker.onError("Failed to attach native class instance data"); + return onJsConstructorError(exceptionTracker, exception); + } + + return object; +} + +static bool nativeClassHasInstance(JSContextRef ctx, + JSObjectRef constructorObject, + JSValueRef possibleInstance, + JSValueRef* exception) { + if (!JSValueIsObject(ctx, possibleInstance)) { + return false; + } + + auto functionData = Valdi::Ref(getAttachedNativeClassFunctionData(constructorObject)); + auto prototype = JSObjectGetProperty( + ctx, + constructorObject, + fromValdiJSPropertyName(functionData->jsContext.getPrototypePropertyName()), + exception); + if (*exception != nullptr) { + return false; + } + + auto current = JSValueToObject(ctx, possibleInstance, exception); + if (*exception != nullptr) { + return false; + } + + while (true) { + auto parent = JSObjectGetPrototype(ctx, current); + if (JSValueIsStrictEqual(ctx, parent, prototype)) { + return true; + } + if (!JSValueIsObject(ctx, parent)) { + return false; + } + current = JSValueToObject(ctx, parent, exception); + if (*exception != nullptr) { + return false; + } + } +} + +static JSValueRef callNativeClassInstanceMember(JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + auto functionData = Valdi::Ref(getAttachedNativeClassFunctionData(function)); + auto& jsContext = functionData->jsContext; + + Valdi::JSValueRef outArguments[argumentCount]; + for (size_t i = 0; i < argumentCount; i++) { + outArguments[i] = Valdi::JSValueRef::makeUnretained( + jsContext, toValdiJSValue(JSCoreRef(arguments[i], JSValueGetType(ctx, arguments[i])))); + } + + Valdi::JSExceptionTracker exceptionTracker(jsContext); + Valdi::JSFunctionNativeCallContext callContext(jsContext, + argumentCount == 0 ? nullptr : outArguments, + argumentCount, + exceptionTracker, + functionData->referenceInfo); + callContext.setThisValue(toValdiJSValue(JSCoreRef(thisObject, kJSTypeObject))); + + auto wrappedObject = Valdi::Ref(getAttachedWrappedObject(thisObject)); + auto instanceData = Valdi::Ref(dynamic_cast(wrappedObject.get())); + if (instanceData == nullptr || instanceData->getNativeClass() != functionData->nativeClass) { + auto error = jsContext.newError( + "Native class member called with an incompatible receiver", std::nullopt, exceptionTracker); + if (exceptionTracker) { + exceptionTracker.storeException(std::move(error)); + } + return onJsCallError(ctx, exceptionTracker, exception); + } + + if (jsContext.interruptRequested()) { + jsContext.onInterrupt(); + } + + auto result = functionData->callback(instanceData->getOpaque().get(), callContext); + if (VALDI_LIKELY(exceptionTracker)) { + return fromValdiJSValue(result.get()).valueRef; + } + return onJsCallError(ctx, exceptionTracker, exception); +} + +static JSValueRef callNativeClassStaticMember(JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + auto functionData = Valdi::Ref(getAttachedNativeClassFunctionData(function)); + auto& jsContext = functionData->jsContext; + + Valdi::JSValueRef outArguments[argumentCount]; + for (size_t i = 0; i < argumentCount; i++) { + outArguments[i] = Valdi::JSValueRef::makeUnretained( + jsContext, toValdiJSValue(JSCoreRef(arguments[i], JSValueGetType(ctx, arguments[i])))); + } + + Valdi::JSExceptionTracker exceptionTracker(jsContext); + Valdi::JSFunctionNativeCallContext callContext(jsContext, + argumentCount == 0 ? nullptr : outArguments, + argumentCount, + exceptionTracker, + functionData->referenceInfo); + callContext.setThisValue(toValdiJSValue(JSCoreRef(thisObject, kJSTypeObject))); + + if (jsContext.interruptRequested()) { + jsContext.onInterrupt(); + } + + auto result = functionData->callback(functionData->nativeClass->getOpaque().get(), callContext); + if (VALDI_LIKELY(exceptionTracker)) { + return fromValdiJSValue(result.get()).valueRef; + } + return onJsCallError(ctx, exceptionTracker, exception); +} + void finalize(JSObjectRef object) { Valdi::RefCountableAutoreleasePool::release(JSObjectGetPrivate(object)); } @@ -91,10 +299,54 @@ JSClassRef getWrappedObjectClassRef() { return _wrappedObjectClassRef; } +static JSClassRef makeNativeClassFunctionClassRef(JSObjectCallAsFunctionCallback callback) { + auto classDefinition = kJSClassDefinitionEmpty; + classDefinition.attributes = kJSClassAttributeNoAutomaticPrototype; + classDefinition.callAsFunction = callback; + classDefinition.finalize = &finalize; + return JSClassCreate(&classDefinition); +} + +JSClassRef JavaScriptCoreContext::getNativeClassConstructorClassRef() { + static const auto classRef = [] { + auto classDefinition = kJSClassDefinitionEmpty; + classDefinition.attributes = kJSClassAttributeNoAutomaticPrototype; + classDefinition.callAsFunction = &callNativeClassAsFunction; + classDefinition.callAsConstructor = &callNativeClassConstructor; + classDefinition.hasInstance = &nativeClassHasInstance; + classDefinition.finalize = &finalize; + return JSClassCreate(&classDefinition); + }(); + return classRef; +} + +JSClassRef getNativeClassInstanceMemberClassRef() { + static auto classRef = makeNativeClassFunctionClassRef(&callNativeClassInstanceMember); + return classRef; +} + +JSClassRef getNativeClassStaticMemberClassRef() { + static auto classRef = makeNativeClassFunctionClassRef(&callNativeClassStaticMember); + return classRef; +} + JSFunctionData::JSFunctionData(JavaScriptCoreContext& jsContext, const Valdi::Ref& function) : jsContext(jsContext), function(function) {} JSFunctionData::~JSFunctionData() = default; +NativeClassFunctionData::NativeClassFunctionData(JavaScriptCoreContext& jsContext, + const Valdi::Ref& nativeClass) + : jsContext(jsContext), + nativeClass(nativeClass), + referenceInfo(nativeClass->getConstructorReferenceInfo()) {} + +NativeClassFunctionData::NativeClassFunctionData(JavaScriptCoreContext& jsContext, + const Valdi::Ref& nativeClass, + const Valdi::StringBox& name) + : jsContext(jsContext), + nativeClass(nativeClass), + referenceInfo(nativeClass->makeMemberReferenceInfo(name)) {} + inline Valdi::RefCountable* getAttachedRefCountable(JSObjectRef objectRef) { return reinterpret_cast(JSObjectGetPrivate(objectRef)); } @@ -104,12 +356,20 @@ JSFunctionData* getAttachedJsFunctionData(JSObjectRef objectRef) { } Valdi::RefCountable* getAttachedWrappedObject(JSObjectRef objectRef) { + if (objectRef == nullptr) { + return nullptr; + } auto* refCountable = getAttachedRefCountable(objectRef); - if (dynamic_cast(refCountable) != nullptr) { - // If our private data is a JSFunctionData, we don't consider it as a wrapped object + if (dynamic_cast(refCountable) != nullptr || + dynamic_cast(refCountable) != nullptr) { + // Function bridge metadata is not a wrapped object. return nullptr; } return refCountable; } +NativeClassFunctionData* getAttachedNativeClassFunctionData(JSObjectRef objectRef) { + return dynamic_cast(getAttachedRefCountable(objectRef)); +} + } // namespace ValdiJSCore diff --git a/valdi/src/valdi/jscore/JSCoreCustomClasses.hpp b/valdi/src/valdi/jscore/JSCoreCustomClasses.hpp index 55e6f7e5..cbf081d4 100644 --- a/valdi/src/valdi/jscore/JSCoreCustomClasses.hpp +++ b/valdi/src/valdi/jscore/JSCoreCustomClasses.hpp @@ -9,12 +9,16 @@ #pragma once #include "valdi/runtime/JavaScript/JavaScriptTypes.hpp" +#include "valdi/runtime/JavaScript/JSNativeClassData.hpp" +#include "valdi_core/cpp/Utils/ReferenceInfo.hpp" #include namespace ValdiJSCore { JSClassRef getNativeFunctionClassRef(); JSClassRef getWrappedObjectClassRef(); +JSClassRef getNativeClassInstanceMemberClassRef(); +JSClassRef getNativeClassStaticMemberClassRef(); class JavaScriptCoreContext; @@ -30,4 +34,20 @@ struct JSFunctionData : public Valdi::SimpleRefCountable { JSFunctionData* getAttachedJsFunctionData(JSObjectRef objectRef); Valdi::RefCountable* getAttachedWrappedObject(JSObjectRef objectRef); +struct NativeClassFunctionData final : public Valdi::SimpleRefCountable { + NativeClassFunctionData(JavaScriptCoreContext& jsContext, + const Valdi::Ref& nativeClass); + + NativeClassFunctionData(JavaScriptCoreContext& jsContext, + const Valdi::Ref& nativeClass, + const Valdi::StringBox& name); + + JavaScriptCoreContext& jsContext; + Valdi::Ref nativeClass; + Valdi::ReferenceInfo referenceInfo; + Valdi::JSClassCallback callback = nullptr; +}; + +NativeClassFunctionData* getAttachedNativeClassFunctionData(JSObjectRef objectRef); + } // namespace ValdiJSCore diff --git a/valdi/src/valdi/jscore/JavaScriptCoreContext.cpp b/valdi/src/valdi/jscore/JavaScriptCoreContext.cpp index 158187a7..6a68881c 100644 --- a/valdi/src/valdi/jscore/JavaScriptCoreContext.cpp +++ b/valdi/src/valdi/jscore/JavaScriptCoreContext.cpp @@ -21,7 +21,6 @@ #include "valdi_core/cpp/Constants.hpp" #include "valdi_core/cpp/Utils/LoggerUtils.hpp" #include "valdi_core/cpp/Utils/StaticString.hpp" -#include "valdi_core/cpp/Utils/StringCache.hpp" #include "valdi_core/cpp/Utils/ValueTypedArray.hpp" #include @@ -156,6 +155,9 @@ JavaScriptCoreContext::~JavaScriptCoreContext() { JSValueUnprotect(_globalContext, microtask); } + _nativeClassDefineAccessor = Valdi::JSValueRef(); + _prototypePropertyName = Valdi::JSPropertyNameRef(); + prepareForTeardown(); auto globalContext = _globalContext; @@ -169,6 +171,8 @@ JavaScriptCoreContext::~JavaScriptCoreContext() { } void JavaScriptCoreContext::onInitialize(Valdi::JSExceptionTracker& exceptionTracker) { + _prototypePropertyName = newPropertyName("prototype"); + auto globalRef = getGlobalObject(exceptionTracker); if (!exceptionTracker) { return; @@ -242,6 +246,19 @@ void JavaScriptCoreContext::onInitialize(Valdi::JSExceptionTracker& exceptionTra _globalContext, fromValdiJSValue(callback.get()).asObjectRef(), nullptr); } } + + _nativeClassDefineAccessor = + ensureRetainedValue(evaluate("(function(object, name, getter, setter, enumerable, configurable) {" + " const descriptor = { enumerable, configurable };" + " if (getter !== undefined) descriptor.get = getter;" + " if (setter !== undefined) descriptor.set = setter;" + " Object.defineProperty(object, name, descriptor);" + "})", + "ValdiNativeClassAccessor.js", + exceptionTracker)); + if (!exceptionTracker) { + return; + } } inline JSContextRef JavaScriptCoreContext::getJSGlobalContext() const { @@ -253,6 +270,10 @@ Valdi::JSValueRef JavaScriptCoreContext::getGlobalObject(Valdi::JSExceptionTrack return returnJSValueRef(globalObject, kJSTypeObject); } +const Valdi::JSPropertyName& JavaScriptCoreContext::getPrototypePropertyName() const { + return _prototypePropertyName.get(); +} + Valdi::BytesView JavaScriptCoreContext::preCompile(const std::string_view& script, const std::string_view& sourceFilename, Valdi::JSExceptionTracker& exceptionTracker) { @@ -496,6 +517,243 @@ Valdi::JSValueRef JavaScriptCoreContext::newWrappedObject(const Valdi::Ref& functionData, + JSClassRef classRef, + std::string_view name, + Valdi::JSExceptionTracker& exceptionTracker) { + auto context = getJSGlobalContext(); + auto object = JSObjectMake(context, classRef, Valdi::unsafeBridgeRetain(functionData.get())); + auto nameValue = newStringUTF8(name, exceptionTracker); + if (!exceptionTracker) { + return Valdi::JSValueRef(); + } + auto nameProperty = newPropertyName("name"); + JSValueRef exception = nullptr; + JSObjectSetProperty(context, + object, + fromValdiJSPropertyName(nameProperty.get()), + fromValdiJSValue(nameValue.get()).valueRef, + kJSPropertyAttributeDontEnum, + &exception); + if (exception != nullptr) { + storeException(exceptionTracker, exception); + return Valdi::JSValueRef(); + } + JSObjectSetPrototype(context, object, _functionPrototype); + return returnJSValueRef(object, kJSTypeObject); +} + +static JSPropertyAttributes toJSCorePropertyAttributes(bool writable, bool enumerable, bool configurable) { + unsigned attributes = kJSPropertyAttributeNone; + if (!writable) { + attributes |= kJSPropertyAttributeReadOnly; + } + if (!enumerable) { + attributes |= kJSPropertyAttributeDontEnum; + } + if (!configurable) { + attributes |= kJSPropertyAttributeDontDelete; + } + return static_cast(attributes); +} + +Valdi::JSValueRef JavaScriptCoreContext::newNativeClass(const Valdi::Ref& classOpaque, + const Valdi::JSClassDefinition& classDefinition, + Valdi::JSExceptionTracker& exceptionTracker) { + std::vector retainedConstants; + for (const auto& entry : classDefinition.getEntries()) { + if (entry.getKind() == Valdi::JSClassEntryKind::Constant) { + retainedConstants.emplace_back(Valdi::JSValueRef::makeRetained(*this, entry.getValue().get())); + } + } + + auto nativeClass = Valdi::makeShared( + classDefinition.getName(), classOpaque, classDefinition.getConstructor()); + auto constructorData = Valdi::makeShared(*this, nativeClass); + auto cls = newNativeClassFunction( + constructorData, + getNativeClassConstructorClassRef(), + classDefinition.getName().toStringView(), + exceptionTracker); + if (!exceptionTracker) { + return Valdi::JSValueRef(); + } + + auto prototype = newObject(exceptionTracker); + if (!exceptionTracker) { + return Valdi::JSValueRef(); + } + auto prototypeObject = fromValdiJSValue(prototype.get()).asObjectRefOrThrow(*this, exceptionTracker); + auto classObject = fromValdiJSValue(cls.get()).asObjectRefOrThrow(*this, exceptionTracker); + if (!exceptionTracker) { + return Valdi::JSValueRef(); + } + + auto defineDataProperty = [&](JSObjectRef object, + std::string_view name, + const Valdi::JSValue& value, + bool writable, + bool enumerable, + bool configurable) -> bool { + auto propertyName = newPropertyName(name); + JSValueRef exception = nullptr; + JSObjectSetProperty(getJSGlobalContext(), + object, + fromValdiJSPropertyName(propertyName.get()), + fromValdiJSValue(value).valueRef, + toJSCorePropertyAttributes(writable, enumerable, configurable), + &exception); + if (exception != nullptr) { + storeException(exceptionTracker, exception); + return false; + } + return true; + }; + + auto defineAccessorProperty = [&](JSObjectRef object, + std::string_view name, + const Valdi::JSValueRef& getter, + const Valdi::JSValueRef& setter, + bool hasGetter, + bool hasSetter, + bool enumerable, + bool configurable) -> bool { + Valdi::JSValueRef arguments[] = { + Valdi::JSValueRef::makeUnretained(*this, toValdiJSValue(JSCoreRef(object, kJSTypeObject))), + newStringUTF8(name, exceptionTracker), + hasGetter ? Valdi::JSValueRef::makeUnretained(*this, getter.get()) : newUndefined(), + hasSetter ? Valdi::JSValueRef::makeUnretained(*this, setter.get()) : newUndefined(), + newBool(enumerable), + newBool(configurable), + }; + if (!exceptionTracker) { + return false; + } + Valdi::JSFunctionCallContext callContext(*this, arguments, 6, exceptionTracker); + callObjectAsFunction(_nativeClassDefineAccessor.get(), callContext); + return static_cast(exceptionTracker); + }; + + if (!defineDataProperty(classObject, "prototype", prototype.get(), false, false, false) || + !defineDataProperty(prototypeObject, "constructor", cls.get(), true, false, true)) { + return Valdi::JSValueRef(); + } + + for (const auto& entry : classDefinition.getEntries()) { + auto object = entry.isClassMember() ? classObject : prototypeObject; + auto memberClassRef = + entry.isClassMember() ? getNativeClassStaticMemberClassRef() : getNativeClassInstanceMemberClassRef(); + switch (entry.getKind()) { + case Valdi::JSClassEntryKind::Method: { + if (entry.getMethodCallback() == nullptr) { + exceptionTracker.onError("Native class method callback cannot be null"); + return Valdi::JSValueRef(); + } + const auto& propertyName = entry.getName(); + auto functionData = Valdi::makeShared(*this, nativeClass, propertyName); + functionData->callback = entry.getMethodCallback(); + auto function = newNativeClassFunction( + functionData, memberClassRef, propertyName.toStringView(), exceptionTracker); + if (!exceptionTracker || !defineDataProperty(object, + propertyName.toStringView(), + function.get(), + entry.isWritable(), + entry.isEnumerable(), + entry.isConfigurable())) { + return Valdi::JSValueRef(); + } + break; + } + case Valdi::JSClassEntryKind::Constant: + if (!defineDataProperty(object, + entry.getName().toStringView(), + entry.getValue().get(), + entry.isWritable(), + entry.isEnumerable(), + entry.isConfigurable())) { + return Valdi::JSValueRef(); + } + break; + case Valdi::JSClassEntryKind::Accessor: { + if (entry.getGetterCallback() == nullptr && entry.getSetterCallback() == nullptr) { + exceptionTracker.onError("Native class accessor must have a getter or setter"); + return Valdi::JSValueRef(); + } + Valdi::JSValueRef getter; + Valdi::JSValueRef setter; + const auto& propertyName = entry.getName(); + if (entry.getGetterCallback() != nullptr) { + auto functionData = + Valdi::makeShared(*this, nativeClass, propertyName); + functionData->callback = entry.getGetterCallback(); + getter = newNativeClassFunction(functionData, + memberClassRef, + propertyName.toStringView(), + exceptionTracker); + } + if (exceptionTracker && entry.getSetterCallback() != nullptr) { + auto functionData = + Valdi::makeShared(*this, nativeClass, propertyName); + functionData->callback = entry.getSetterCallback(); + setter = newNativeClassFunction(functionData, + memberClassRef, + propertyName.toStringView(), + exceptionTracker); + } + if (!exceptionTracker || !defineAccessorProperty(object, + propertyName.toStringView(), + getter, + setter, + entry.getGetterCallback() != nullptr, + entry.getSetterCallback() != nullptr, + entry.isEnumerable(), + entry.isConfigurable())) { + return Valdi::JSValueRef(); + } + break; + } + } + } + + return cls; +} + +Valdi::JSValueRef JavaScriptCoreContext::newObjectFromNativeClass(const Valdi::Ref& opaque, + const Valdi::JSValue& cls, + Valdi::JSExceptionTracker& exceptionTracker) { + if (opaque == nullptr) { + exceptionTracker.onError("Native class opaque object cannot be null"); + return Valdi::JSValueRef(); + } + + auto classObject = fromValdiJSValue(cls).asObjectRefOrThrow(*this, exceptionTracker); + if (!exceptionTracker) { + return Valdi::JSValueRef(); + } + auto constructorData = getAttachedNativeClassFunctionData(classObject); + if (constructorData == nullptr) { + exceptionTracker.onError("Value is not a native class"); + return Valdi::JSValueRef(); + } + JSValueRef exception = nullptr; + auto prototypeValue = JSObjectGetProperty( + getJSGlobalContext(), classObject, fromValdiJSPropertyName(_prototypePropertyName.get()), &exception); + if (exception != nullptr) { + storeException(exceptionTracker, exception); + return Valdi::JSValueRef(); + } + auto prototypeObject = JSValueToObject(getJSGlobalContext(), prototypeValue, &exception); + if (exception != nullptr) { + storeException(exceptionTracker, exception); + return Valdi::JSValueRef(); + } + auto instanceData = Valdi::makeShared(constructorData->nativeClass, opaque); + auto object = + JSObjectMake(getJSGlobalContext(), getWrappedObjectClassRef(), Valdi::unsafeBridgeRetain(instanceData.get())); + JSObjectSetPrototype(getJSGlobalContext(), object, prototypeObject); + return returnJSValueRef(object, kJSTypeObject); +} + Valdi::JSValueRef JavaScriptCoreContext::newWeakRef(const Valdi::JSValue& object, Valdi::JSExceptionTracker& exceptionTracker) { if (_weakRefConstructor == nullptr) { @@ -596,7 +854,7 @@ Valdi::Ref JavaScriptCoreContext::valueToWrappedObject( if (objectRef == nullptr) { return nullptr; } - return Valdi::Ref(getAttachedWrappedObject(objectRef)); + return Valdi::unwrapNativeClassInstanceData(Valdi::Ref(getAttachedWrappedObject(objectRef))); } Valdi::Ref JavaScriptCoreContext::valueToFunction(const Valdi::JSValue& value, diff --git a/valdi/src/valdi/jscore/JavaScriptCoreContext.hpp b/valdi/src/valdi/jscore/JavaScriptCoreContext.hpp index 329605d6..435c8fff 100644 --- a/valdi/src/valdi/jscore/JavaScriptCoreContext.hpp +++ b/valdi/src/valdi/jscore/JavaScriptCoreContext.hpp @@ -18,6 +18,8 @@ class ILogger; namespace ValdiJSCore { +struct NativeClassFunctionData; + class JavaScriptCoreContext : public Valdi::IJavaScriptContext { public: explicit JavaScriptCoreContext(Valdi::JavaScriptTaskScheduler* taskScheduler, Valdi::ILogger& logger); @@ -66,6 +68,14 @@ class JavaScriptCoreContext : public Valdi::IJavaScriptContext { Valdi::JSValueRef newWrappedObject(const Valdi::Ref& wrappedObject, Valdi::JSExceptionTracker& exceptionTracker) override; + Valdi::JSValueRef newNativeClass(const Valdi::Ref& classOpaque, + const Valdi::JSClassDefinition& classDefinition, + Valdi::JSExceptionTracker& exceptionTracker) override; + + Valdi::JSValueRef newObjectFromNativeClass(const Valdi::Ref& opaque, + const Valdi::JSValue& cls, + Valdi::JSExceptionTracker& exceptionTracker) override; + Valdi::JSValueRef newWeakRef(const Valdi::JSValue& object, Valdi::JSExceptionTracker& exceptionTracker) override; Valdi::JSValueRef getObjectProperty(const Valdi::JSValue& object, @@ -166,6 +176,8 @@ class JavaScriptCoreContext : public Valdi::IJavaScriptContext { void willEnterVM() override; void willExitVM(Valdi::JSExceptionTracker& exceptionTracker) override; + const Valdi::JSPropertyName& getPrototypePropertyName() const; + protected: Valdi::JSValueRef onNewBool(bool boolean) override; @@ -183,11 +195,13 @@ class JavaScriptCoreContext : public Valdi::IJavaScriptContext { [[maybe_unused]] Valdi::ILogger& _logger; JSContextGroupRef _contextGroup; JSGlobalContextRef _globalContext; + Valdi::JSPropertyNameRef _prototypePropertyName; JSValueRef _functionPrototype = nullptr; JSObjectRef _errorConstructor = nullptr; JSObjectRef _emptyArrayBuffer = nullptr; JSObjectRef _weakRefConstructor = nullptr; JSObjectRef _weakRefDerefFunction = nullptr; + Valdi::JSValueRef _nativeClassDefineAccessor; bool _needGarbageCollect = false; bool _isGarbageCollecting = false; [[maybe_unused]] uint16_t _debuggerPort = 0; @@ -205,6 +219,13 @@ class JavaScriptCoreContext : public Valdi::IJavaScriptContext { Valdi::JSValueRef returnJSValueRef(const JSValueRef& value, JSType type); void storeException(Valdi::JSExceptionTracker& exceptionTracker, const JSValueRef& exception); + + Valdi::JSValueRef newNativeClassFunction(const Valdi::Ref& functionData, + JSClassRef classRef, + std::string_view name, + Valdi::JSExceptionTracker& exceptionTracker); + + static JSClassRef getNativeClassConstructorClassRef(); }; } // namespace ValdiJSCore diff --git a/valdi/src/valdi/quickjs/QuickJSJavaScriptContext.cpp b/valdi/src/valdi/quickjs/QuickJSJavaScriptContext.cpp index e568e6a6..36f1b147 100644 --- a/valdi/src/valdi/quickjs/QuickJSJavaScriptContext.cpp +++ b/valdi/src/valdi/quickjs/QuickJSJavaScriptContext.cpp @@ -17,6 +17,7 @@ #include "valdi_core/cpp/Utils/ByteBuffer.hpp" #include "valdi_core/cpp/Utils/Format.hpp" #include "valdi_core/cpp/Utils/StaticString.hpp" +#include "valdi_core/cpp/Utils/StringCache.hpp" #include "tsn/tsn.h" @@ -194,6 +195,20 @@ void QuickJSJavaScriptContext::onInitialize(Valdi::JSExceptionTracker& exception return; } + _nativeClassConstructorClassID = initializeClass(getNativeClassConstructorClassDef(), exceptionTracker); + if (!exceptionTracker) { + return; + } + + initializeClass(getNativeClassInstanceMemberClassDef(), exceptionTracker); + if (!exceptionTracker) { + return; + } + initializeClass(getNativeClassStaticMemberClassDef(), exceptionTracker); + if (!exceptionTracker) { + return; + } + /** We capture the Object.prototype property to optimize the visitObjectPropertyNames implementation such that we stop the loop through the prototype chain until @@ -601,6 +616,217 @@ Valdi::JSValueRef QuickJSJavaScriptContext::newWrappedObject(const Valdi::Ref& functionData, + const JSClassDefWithId* classDef, + std::string_view name, + Valdi::JSExceptionTracker& exceptionTracker) { + auto function = checkCallAndGetValue( + exceptionTracker, JS_NewObjectProtoClass(_context, _functionPrototype, classDef->classID)); + if (!exceptionTracker) { + return Valdi::JSValueRef(); + } + + setNativeClassFunctionData(fromValdiJSValue(function.get()), functionData); + auto nameValue = newStringUTF8(name, exceptionTracker); + if (!exceptionTracker) { + return Valdi::JSValueRef(); + } + + checkCall(exceptionTracker, + JS_DefinePropertyValueStr(_context, + fromValdiJSValue(function.get()), + "name", + JS_DupValue(_context, fromValdiJSValue(nameValue.get())), + JS_PROP_CONFIGURABLE | JS_PROP_THROW)); + return function; +} + +Valdi::JSValueRef QuickJSJavaScriptContext::newNativeClass(const Valdi::Ref& classOpaque, + const Valdi::JSClassDefinition& classDefinition, + Valdi::JSExceptionTracker& exceptionTracker) { + auto guard = _threadAccessChecker.guard(); + auto nativeClass = Valdi::makeShared( + classDefinition.getName(), classOpaque, classDefinition.getConstructor()); + + auto prototype = checkCallAndGetValue(exceptionTracker, JS_NewObject(_context)); + if (!exceptionTracker) { + return Valdi::JSValueRef(); + } + + auto constructor = checkCallAndGetValue( + exceptionTracker, JS_NewObjectProtoClass(_context, _functionPrototype, _nativeClassConstructorClassID)); + if (!exceptionTracker) { + return Valdi::JSValueRef(); + } + setNativeClassConstructorData(fromValdiJSValue(constructor.get()), nativeClass); + JS_SetConstructorBit(_context, fromValdiJSValue(constructor.get()), true); + + auto className = newStringUTF8(classDefinition.getName().toStringView(), exceptionTracker); + if (!exceptionTracker) { + return Valdi::JSValueRef(); + } + if (!checkCall(exceptionTracker, + JS_DefinePropertyValueStr(_context, + fromValdiJSValue(constructor.get()), + "name", + JS_DupValue(_context, fromValdiJSValue(className.get())), + JS_PROP_CONFIGURABLE | JS_PROP_THROW))) { + return Valdi::JSValueRef(); + } + + JS_SetConstructor(_context, fromValdiJSValue(constructor.get()), fromValdiJSValue(prototype.get())); + + auto defineDataProperty = [&](JSValueConst object, + std::string_view name, + JSValue value, + bool writable, + bool enumerable, + bool configurable) -> bool { + auto atom = JS_NewAtomLen(_context, name.data(), name.size()); + auto result = JS_DefinePropertyValue( + _context, object, atom, value, toQuickJSPropertyFlags(writable, enumerable, configurable)); + JS_FreeAtom(_context, atom); + return checkCall(exceptionTracker, result); + }; + + for (const auto& entry : classDefinition.getEntries()) { + auto object = + entry.isClassMember() ? fromValdiJSValue(constructor.get()) : fromValdiJSValue(prototype.get()); + auto* memberClassDef = + entry.isClassMember() ? getNativeClassStaticMemberClassDef() : getNativeClassInstanceMemberClassDef(); + switch (entry.getKind()) { + case Valdi::JSClassEntryKind::Method: { + if (entry.getMethodCallback() == nullptr) { + exceptionTracker.onError("Native class method callback cannot be null"); + return Valdi::JSValueRef(); + } + const auto& propertyName = entry.getName(); + auto functionData = Valdi::makeShared(nativeClass, propertyName); + functionData->callback = entry.getMethodCallback(); + auto function = newNativeClassFunction( + functionData, memberClassDef, propertyName.toStringView(), exceptionTracker); + if (!exceptionTracker || + !defineDataProperty(object, + propertyName.toStringView(), + JS_DupValue(_context, fromValdiJSValue(function.get())), + entry.isWritable(), + entry.isEnumerable(), + entry.isConfigurable())) { + return Valdi::JSValueRef(); + } + break; + } + case Valdi::JSClassEntryKind::Constant: + if (!defineDataProperty(object, + entry.getName().toStringView(), + JS_DupValue(_context, fromValdiJSValue(entry.getValue().get())), + entry.isWritable(), + entry.isEnumerable(), + entry.isConfigurable())) { + return Valdi::JSValueRef(); + } + break; + case Valdi::JSClassEntryKind::Accessor: { + if (entry.getGetterCallback() == nullptr && entry.getSetterCallback() == nullptr) { + exceptionTracker.onError("Native class accessor must have a getter or setter"); + return Valdi::JSValueRef(); + } + + Valdi::JSValueRef getter; + Valdi::JSValueRef setter; + const auto& propertyName = entry.getName(); + if (entry.getGetterCallback() != nullptr) { + auto functionData = Valdi::makeShared(nativeClass, propertyName); + functionData->callback = entry.getGetterCallback(); + getter = newNativeClassFunction(functionData, + memberClassDef, + propertyName.toStringView(), + exceptionTracker); + } + if (exceptionTracker && entry.getSetterCallback() != nullptr) { + auto functionData = Valdi::makeShared(nativeClass, propertyName); + functionData->callback = entry.getSetterCallback(); + setter = newNativeClassFunction(functionData, + memberClassDef, + propertyName.toStringView(), + exceptionTracker); + } + if (!exceptionTracker) { + return Valdi::JSValueRef(); + } + + auto getterValue = + getter.empty() ? JS_UNDEFINED : JS_DupValue(_context, fromValdiJSValue(getter.get())); + auto setterValue = + setter.empty() ? JS_UNDEFINED : JS_DupValue(_context, fromValdiJSValue(setter.get())); + auto propertyNameView = propertyName.toStringView(); + auto atom = JS_NewAtomLen(_context, propertyNameView.data(), propertyNameView.size()); + auto result = + JS_DefinePropertyGetSet(_context, + object, + atom, + getterValue, + setterValue, + toQuickJSPropertyFlags( + false, entry.isEnumerable(), entry.isConfigurable())); + JS_FreeAtom(_context, atom); + if (!checkCall(exceptionTracker, result)) { + return Valdi::JSValueRef(); + } + break; + } + } + } + + return constructor; +} + +Valdi::JSValueRef QuickJSJavaScriptContext::newObjectFromNativeClass(const Valdi::Ref& opaque, + const Valdi::JSValue& cls, + Valdi::JSExceptionTracker& exceptionTracker) { + auto guard = _threadAccessChecker.guard(); + auto clsValue = fromValdiJSValue(cls); + auto nativeClass = getNativeClassConstructorData(clsValue); + if (nativeClass == nullptr) { + exceptionTracker.onError("Value is not a native class"); + return Valdi::JSValueRef(); + } + if (opaque == nullptr) { + exceptionTracker.onError("Native class opaque object cannot be null"); + return Valdi::JSValueRef(); + } + + auto prototype = checkCallAndGetValue(exceptionTracker, JS_GetPropertyStr(_context, clsValue, "prototype")); + if (!exceptionTracker) { + return Valdi::JSValueRef(); + } + + auto object = checkCallAndGetValue( + exceptionTracker, JS_NewObjectProtoClass(_context, fromValdiJSValue(prototype.get()), _wrappedObjectClassID)); + if (!exceptionTracker) { + return Valdi::JSValueRef(); + } + + auto instanceData = Valdi::makeShared(nativeClass, opaque); + setObjectWrappedObject(fromValdiJSValue(object.get()), instanceData); + return object; +} + Valdi::JSValueRef QuickJSJavaScriptContext::getObjectProperty(const Valdi::JSValue& object, const std::string_view& propertyName, Valdi::JSExceptionTracker& exceptionTracker) { @@ -869,7 +1095,7 @@ int32_t QuickJSJavaScriptContext::valueToInt(const Valdi::JSValue& value, Valdi: Valdi::Ref QuickJSJavaScriptContext::valueToWrappedObject( const Valdi::JSValue& value, Valdi::JSExceptionTracker& /*exceptionTracker*/) { auto guard = _threadAccessChecker.guard(); - return getObjectWrappedObject(fromValdiJSValue(value)); + return Valdi::unwrapNativeClassInstanceData(getObjectWrappedObject(fromValdiJSValue(value))); } Valdi::Ref QuickJSJavaScriptContext::valueToFunction( diff --git a/valdi/src/valdi/quickjs/QuickJSJavaScriptContext.hpp b/valdi/src/valdi/quickjs/QuickJSJavaScriptContext.hpp index ac80434d..55d343f2 100644 --- a/valdi/src/valdi/quickjs/QuickJSJavaScriptContext.hpp +++ b/valdi/src/valdi/quickjs/QuickJSJavaScriptContext.hpp @@ -28,6 +28,7 @@ class WeakReference; class QuickJSJavaScriptContextEntry; struct JSClassDefWithId; +struct NativeClassFunctionData; struct QuickJSRejectedPromise { JSValue promise; @@ -86,6 +87,14 @@ class QuickJSJavaScriptContext : public Valdi::IJavaScriptContext { Valdi::JSValueRef newWrappedObject(const Valdi::Ref& wrappedObject, Valdi::JSExceptionTracker& exceptionTracker) override; + Valdi::JSValueRef newNativeClass(const Valdi::Ref& classOpaque, + const Valdi::JSClassDefinition& classDefinition, + Valdi::JSExceptionTracker& exceptionTracker) override; + + Valdi::JSValueRef newObjectFromNativeClass(const Valdi::Ref& opaque, + const Valdi::JSValue& cls, + Valdi::JSExceptionTracker& exceptionTracker) override; + Valdi::JSValueRef newWeakRef(const Valdi::JSValue& object, Valdi::JSExceptionTracker& exceptionTracker) override; Valdi::JSValueRef getObjectProperty(const Valdi::JSValue& object, @@ -229,6 +238,7 @@ class QuickJSJavaScriptContext : public Valdi::IJavaScriptContext { JSClassID _functionClassID = 0; JSClassID _wrappedObjectClassID = 0; JSClassID _weakReferenceFinalizerClassID = 0; + JSClassID _nativeClassConstructorClassID = 0; size_t _enterVmCount = 0; size_t _weakReferenceSequence = 0; bool _needsGarbageCollect = false; @@ -264,6 +274,11 @@ class QuickJSJavaScriptContext : public Valdi::IJavaScriptContext { Valdi::JSValueRef newTypedArrayFromConstructor(const JSValue& ctor, const Valdi::JSValue& arrayBuffer, Valdi::JSExceptionTracker& exceptionTracker); + + Valdi::JSValueRef newNativeClassFunction(const Valdi::Ref& functionData, + const JSClassDefWithId* classDef, + std::string_view name, + Valdi::JSExceptionTracker& exceptionTracker); }; } // namespace ValdiQuickJS diff --git a/valdi/src/valdi/quickjs/QuickJSUtils.cpp b/valdi/src/valdi/quickjs/QuickJSUtils.cpp index 908e8ebd..a0c81263 100644 --- a/valdi/src/valdi/quickjs/QuickJSUtils.cpp +++ b/valdi/src/valdi/quickjs/QuickJSUtils.cpp @@ -24,6 +24,135 @@ static JSValue onJsCallError(JSContext* context, Valdi::JSExceptionTracker& exce return JS_Throw(context, JS_DupValue(context, fromValdiJSValue(exception.get()))); } +NativeClassFunctionData::NativeClassFunctionData(const Valdi::Ref& nativeClass, + const Valdi::StringBox& name) + : nativeClass(nativeClass), + referenceInfo(nativeClass->makeMemberReferenceInfo(name)) {} + +static JSValue callNativeClassInstanceMember(JSContext* context, + JSValueConst funcObject, + JSValueConst thisValue, + int argc, + JSValueConst* argv, + int /*flags*/) { + auto& valdiJsContext = *getValdiJSContext(context); + auto* opaque = JS_GetOpaque(funcObject, getNativeClassInstanceMemberClassDef()->classID); + auto functionData = Valdi::unsafeBridge(opaque); + + Valdi::JSValueRef arguments[argc]; + for (int i = 0; i < argc; i++) { + arguments[i] = Valdi::JSValueRef::makeUnretained(valdiJsContext, toValdiJSValue(argv[i])); + } + + Valdi::JSExceptionTracker exceptionTracker(valdiJsContext); + Valdi::JSFunctionNativeCallContext callContext( + valdiJsContext, arguments, static_cast(argc), exceptionTracker, functionData->referenceInfo); + callContext.setThisValue(toValdiJSValue(thisValue)); + + auto wrappedObject = getObjectWrappedObject(thisValue); + auto* instanceData = dynamic_cast(wrappedObject.get()); + if (instanceData == nullptr || instanceData->getNativeClass() != functionData->nativeClass) { + return JS_ThrowTypeError(context, "Native class member called with an incompatible receiver"); + } + + if (valdiJsContext.interruptRequested()) { + valdiJsContext.onInterrupt(); + } + + auto result = functionData->callback(instanceData->getOpaque().get(), callContext); + if (VALDI_LIKELY(exceptionTracker)) { + return JS_DupValue(context, fromValdiJSValue(result.get())); + } + return onJsCallError(context, exceptionTracker); +} + +static JSValue callNativeClassStaticMember(JSContext* context, + JSValueConst funcObject, + JSValueConst thisValue, + int argc, + JSValueConst* argv, + int /*flags*/) { + auto& valdiJsContext = *getValdiJSContext(context); + auto* opaque = JS_GetOpaque(funcObject, getNativeClassStaticMemberClassDef()->classID); + auto functionData = Valdi::unsafeBridge(opaque); + + Valdi::JSValueRef arguments[argc]; + for (int i = 0; i < argc; i++) { + arguments[i] = Valdi::JSValueRef::makeUnretained(valdiJsContext, toValdiJSValue(argv[i])); + } + + Valdi::JSExceptionTracker exceptionTracker(valdiJsContext); + Valdi::JSFunctionNativeCallContext callContext( + valdiJsContext, arguments, static_cast(argc), exceptionTracker, functionData->referenceInfo); + callContext.setThisValue(toValdiJSValue(thisValue)); + + if (valdiJsContext.interruptRequested()) { + valdiJsContext.onInterrupt(); + } + + auto result = functionData->callback(functionData->nativeClass->getOpaque().get(), callContext); + if (VALDI_LIKELY(exceptionTracker)) { + return JS_DupValue(context, fromValdiJSValue(result.get())); + } + return onJsCallError(context, exceptionTracker); +} + +static JSValue callNativeClassConstructor( + JSContext* context, JSValueConst funcObject, JSValueConst newTarget, int argc, JSValueConst* argv, int flags) { + if ((flags & JS_CALL_FLAG_CONSTRUCTOR) == 0) { + return JS_ThrowTypeError(context, "Native class constructor must be called with new"); + } + if (JS_VALUE_GET_PTR(funcObject) != JS_VALUE_GET_PTR(newTarget)) { + return JS_ThrowTypeError(context, "Native class subclassing is not supported"); + } + + auto& valdiJsContext = *getValdiJSContext(context); + auto nativeClass = getNativeClassConstructorData(funcObject); + auto constructor = nativeClass->getConstructor(); + if (constructor == nullptr) { + return JS_ThrowTypeError(context, "Native class cannot be constructed from JavaScript"); + } + + Valdi::JSValueRef arguments[argc]; + for (int i = 0; i < argc; i++) { + arguments[i] = Valdi::JSValueRef::makeUnretained(valdiJsContext, toValdiJSValue(argv[i])); + } + + Valdi::JSExceptionTracker exceptionTracker(valdiJsContext); + const auto& referenceInfo = nativeClass->getConstructorReferenceInfo(); + Valdi::JSFunctionNativeCallContext callContext( + valdiJsContext, arguments, static_cast(argc), exceptionTracker, referenceInfo); + + auto prototype = JS_GetPropertyStr(context, funcObject, "prototype"); + if (JS_IsException(prototype)) { + return prototype; + } + auto object = JS_NewObjectProtoClass(context, prototype, getWrappedObjectClassDef()->classID); + JS_FreeValue(context, prototype); + if (JS_IsException(object)) { + return object; + } + callContext.setThisValue(toValdiJSValue(object)); + + if (valdiJsContext.interruptRequested()) { + valdiJsContext.onInterrupt(); + } + + auto nativeOpaque = constructor(nativeClass->getOpaque().get(), callContext); + if (exceptionTracker && nativeOpaque == nullptr) { + exceptionTracker.onError("Native class constructor returned a null opaque object"); + } + + if (!exceptionTracker) { + JS_FreeValue(context, object); + return onJsCallError(context, exceptionTracker); + } + + auto instanceData = Valdi::makeShared(nativeClass, nativeOpaque); + setObjectWrappedObject(object, instanceData); + return object; +} + JSValue jsCall( JSContext* context, JSValueConst funcObject, JSValueConst thisValue, int argc, JSValueConst* argv, int /*flags*/) { auto& valdiJsContext = *getValdiJSContext(context); @@ -126,6 +255,56 @@ JSClassDefWithId* makeWeakRefFinalizerClassDef() { return classDefWithId; } +void nativeClassConstructorFinalize(JSRuntime* /*rt*/, JSValue value) { + setNativeClassConstructorData(value, nullptr); +} + +static void releaseNativeClassFunctionData(const JSValue& value, const JSClassDefWithId* classDef) { + auto* opaque = JS_GetOpaque(value, classDef->classID); + Valdi::RefCountableAutoreleasePool::release(opaque); +} + +void nativeClassInstanceMemberFinalize(JSRuntime* /*rt*/, JSValue value) { + releaseNativeClassFunctionData(value, getNativeClassInstanceMemberClassDef()); +} + +void nativeClassStaticMemberFinalize(JSRuntime* /*rt*/, JSValue value) { + releaseNativeClassFunctionData(value, getNativeClassStaticMemberClassDef()); +} + +JSClassDefWithId* makeNativeClassConstructorClassDef() { + auto* classDefWithId = newClassDefWithId("NativeClassConstructor"); + classDefWithId->classDef.call = &callNativeClassConstructor; + classDefWithId->classDef.finalizer = &nativeClassConstructorFinalize; + return classDefWithId; +} + +const JSClassDefWithId* getNativeClassConstructorClassDef() { + static auto* kClassDef = makeNativeClassConstructorClassDef(); + return kClassDef; +} + +JSClassDefWithId* makeNativeClassFunctionClassDef(const char* name, + JSClassCall* call, + JSClassFinalizer* finalizer) { + auto* classDefWithId = newClassDefWithId(name); + classDefWithId->classDef.call = call; + classDefWithId->classDef.finalizer = finalizer; + return classDefWithId; +} + +const JSClassDefWithId* getNativeClassInstanceMemberClassDef() { + static auto* kClassDef = makeNativeClassFunctionClassDef( + "NativeClassInstanceMember", &callNativeClassInstanceMember, &nativeClassInstanceMemberFinalize); + return kClassDef; +} + +const JSClassDefWithId* getNativeClassStaticMemberClassDef() { + static auto* kClassDef = makeNativeClassFunctionClassDef( + "NativeClassStaticMember", &callNativeClassStaticMember, &nativeClassStaticMemberFinalize); + return kClassDef; +} + const JSClassDefWithId* getWeakRefFinalizerClassDef() { static auto* kClassDef = makeWeakRefFinalizerClassDef(); @@ -147,6 +326,23 @@ Valdi::Ref getObjectWrappedObject(const JSValue& value) { return Valdi::unsafeBridge(opaque); } +void setNativeClassConstructorData(const JSValue& value, const Valdi::Ref& nativeClass) { + auto* opaque = JS_GetOpaque(value, getNativeClassConstructorClassDef()->classID); + if (opaque != nullptr) { + Valdi::RefCountableAutoreleasePool::release(opaque); + } + JS_SetOpaque(value, Valdi::unsafeBridgeRetain(nativeClass.get())); +} + +Valdi::Ref getNativeClassConstructorData(const JSValue& value) { + auto* opaque = JS_GetOpaque(value, getNativeClassConstructorClassDef()->classID); + return Valdi::unsafeBridge(opaque); +} + +void setNativeClassFunctionData(const JSValue& value, const Valdi::Ref& functionData) { + JS_SetOpaque(value, Valdi::unsafeBridgeRetain(functionData.get())); +} + size_t weakReferenceIdFromJSWeakReferenceFinalizer(const JSValue& jsWeakReferenceFinalizer) { auto* opaque = JS_GetOpaque(jsWeakReferenceFinalizer, getWeakRefFinalizerClassDef()->classID); return reinterpret_cast(opaque); diff --git a/valdi/src/valdi/quickjs/QuickJSUtils.hpp b/valdi/src/valdi/quickjs/QuickJSUtils.hpp index 17e4a48c..a647c5e9 100644 --- a/valdi/src/valdi/quickjs/QuickJSUtils.hpp +++ b/valdi/src/valdi/quickjs/QuickJSUtils.hpp @@ -8,7 +8,9 @@ #pragma once #include "valdi/runtime/Interfaces/IJavaScriptContext.hpp" +#include "valdi/runtime/JavaScript/JSNativeClassData.hpp" #include "valdi/runtime/Utils/RefCountableAutoreleasePool.hpp" +#include "valdi_core/cpp/Utils/ReferenceInfo.hpp" #include "valdi_core/cpp/Utils/Result.hpp" #include @@ -27,6 +29,18 @@ struct JSClassDefWithId { const JSClassDefWithId* getBridgedFunctionClassDef(); const JSClassDefWithId* getWrappedObjectClassDef(); const JSClassDefWithId* getWeakRefFinalizerClassDef(); +const JSClassDefWithId* getNativeClassConstructorClassDef(); +const JSClassDefWithId* getNativeClassInstanceMemberClassDef(); +const JSClassDefWithId* getNativeClassStaticMemberClassDef(); + +struct NativeClassFunctionData final : public Valdi::SimpleRefCountable { + NativeClassFunctionData(const Valdi::Ref& nativeClass, + const Valdi::StringBox& name); + + Valdi::Ref nativeClass; + Valdi::ReferenceInfo referenceInfo; + Valdi::JSClassCallback callback = nullptr; +}; inline Valdi::JSValue toValdiJSValue(const JSValue& value) { return Valdi::JSValue(value); @@ -74,6 +88,11 @@ inline Valdi::JSFunction* getObjectCallable(const JSValue& value) { void setObjectWrappedObject(const JSValue& value, const Valdi::Ref& wrappedObject); Valdi::Ref getObjectWrappedObject(const JSValue& value); +void setNativeClassConstructorData(const JSValue& value, const Valdi::Ref& nativeClass); +Valdi::Ref getNativeClassConstructorData(const JSValue& value); + +void setNativeClassFunctionData(const JSValue& value, const Valdi::Ref& functionData); + size_t weakReferenceIdFromJSWeakReferenceFinalizer(const JSValue& jsWeakReferenceFinalizer); void setWeakReferenceIdToJSWeakReferenceFinalizer(const JSValue& jsWeakReferenceFinalizer, size_t weakReferenceId); diff --git a/valdi/src/valdi/runtime/Interfaces/IJavaScriptContext.cpp b/valdi/src/valdi/runtime/Interfaces/IJavaScriptContext.cpp index f2a940ec..71c449e1 100644 --- a/valdi/src/valdi/runtime/Interfaces/IJavaScriptContext.cpp +++ b/valdi/src/valdi/runtime/Interfaces/IJavaScriptContext.cpp @@ -107,6 +107,19 @@ JSValueRef IJavaScriptContext::newUndefined() { return _undefinedValue.asUnretained(); } +JSValueRef IJavaScriptContext::newString(const StaticString& str, JSExceptionTracker& exceptionTracker) { + switch (str.encoding()) { + case StaticString::Encoding::UTF8: + return newStringUTF8(str.utf8StringView(), exceptionTracker); + case StaticString::Encoding::UTF16: + return newStringUTF16(str.utf16StringView(), exceptionTracker); + case StaticString::Encoding::UTF32: { + auto utf8 = str.utf8Storage(); + return newStringUTF8(utf8.toStringView(), exceptionTracker); + } + } +} + JSValueRef IJavaScriptContext::newArrayWithValues(const JSValue* values, size_t size, JSExceptionTracker& exceptionTracker) { diff --git a/valdi/src/valdi/runtime/Interfaces/IJavaScriptContext.hpp b/valdi/src/valdi/runtime/Interfaces/IJavaScriptContext.hpp index 26dc2bdf..84dd6e9b 100644 --- a/valdi/src/valdi/runtime/Interfaces/IJavaScriptContext.hpp +++ b/valdi/src/valdi/runtime/Interfaces/IJavaScriptContext.hpp @@ -11,6 +11,7 @@ #include "utils/base/NonCopyable.hpp" #include "utils/platform/BuildOptions.hpp" #include "valdi/runtime/JavaScript/JSFunctionExportMode.hpp" +#include "valdi/runtime/JavaScript/JSClassDefinition.hpp" #include "valdi/runtime/JavaScript/JavaScriptLong.hpp" #include "valdi/runtime/JavaScript/JavaScriptTypes.hpp" #include "valdi_core/cpp/Threading/DispatchQueue.hpp" @@ -157,6 +158,8 @@ class IJavaScriptContext : public SharedPtrRefCountable, public snap::NonCopyabl virtual JSValueRef newStringUTF8(const std::string_view& str, JSExceptionTracker& exceptionTracker) = 0; virtual JSValueRef newStringUTF16(const std::u16string_view& str, JSExceptionTracker& exceptionTracker) = 0; + JSValueRef newString(const StaticString& str, JSExceptionTracker& exceptionTracker); + virtual JSValueRef newArray(size_t initialSize, JSExceptionTracker& exceptionTracker) = 0; virtual JSValueRef newArrayWithValues(const JSValue* values, size_t size, JSExceptionTracker& exceptionTracker); @@ -171,6 +174,14 @@ class IJavaScriptContext : public SharedPtrRefCountable, public snap::NonCopyabl virtual JSValueRef newWrappedObject(const Ref& wrappedObject, JSExceptionTracker& exceptionTracker) = 0; + virtual JSValueRef newNativeClass(const Ref& classOpaque, + const JSClassDefinition& classDefinition, + JSExceptionTracker& exceptionTracker) = 0; + + virtual JSValueRef newObjectFromNativeClass(const Ref& opaque, + const JSValue& cls, + JSExceptionTracker& exceptionTracker) = 0; + virtual JSValueRef newWeakRef(const JSValue& object, JSExceptionTracker& exceptionTracker) = 0; JSValueRef newLong(int64_t value, JSExceptionTracker& exceptionTracker); diff --git a/valdi/src/valdi/runtime/JavaScript/JSClassDefinition.cpp b/valdi/src/valdi/runtime/JavaScript/JSClassDefinition.cpp new file mode 100644 index 00000000..37b3d506 --- /dev/null +++ b/valdi/src/valdi/runtime/JavaScript/JSClassDefinition.cpp @@ -0,0 +1,200 @@ +// +// JSClassDefinition.cpp +// ValdiRuntime +// + +#include "valdi/runtime/JavaScript/JSClassDefinition.hpp" + +#include + +namespace Valdi { + +JSClassEntry::JSClassEntry() = default; + +JSClassEntry::~JSClassEntry() = default; + +JSClassEntry JSClassEntry::method(const StringBox& name, JSClassCallback callback) { + return method(name, callback, true, false, true); +} + +JSClassEntry JSClassEntry::method( + const StringBox& name, JSClassCallback callback, bool writable, bool enumerable, bool configurable) { + JSClassEntry entry; + entry._name = name; + entry._kind = JSClassEntryKind::Method; + entry._methodCallback = callback; + entry._writable = writable; + entry._enumerable = enumerable; + entry._configurable = configurable; + return entry; +} + +JSClassEntry JSClassEntry::constant(const StringBox& name, JSValueRef value) { + return constant(name, std::move(value), false, false, false); +} + +JSClassEntry JSClassEntry::constant( + const StringBox& name, JSValueRef value, bool writable, bool enumerable, bool configurable) { + JSClassEntry entry; + entry._name = name; + entry._kind = JSClassEntryKind::Constant; + entry._value = std::move(value); + entry._writable = writable; + entry._enumerable = enumerable; + entry._configurable = configurable; + return entry; +} + +JSClassEntry JSClassEntry::accessor(const StringBox& name, JSClassCallback getter, JSClassCallback setter) { + return accessor(name, getter, setter, false, true); +} + +JSClassEntry JSClassEntry::accessor( + const StringBox& name, JSClassCallback getter, JSClassCallback setter, bool enumerable, bool configurable) { + JSClassEntry entry; + entry._name = name; + entry._kind = JSClassEntryKind::Accessor; + entry._getterCallback = getter; + entry._setterCallback = setter; + entry._writable = false; + entry._enumerable = enumerable; + entry._configurable = configurable; + return entry; +} + +const StringBox& JSClassEntry::getName() const noexcept { + return _name; +} + +JSClassEntryKind JSClassEntry::getKind() const noexcept { + return _kind; +} + +const JSValueRef& JSClassEntry::getValue() const noexcept { + return _value; +} + +JSClassCallback JSClassEntry::getMethodCallback() const noexcept { + return _methodCallback; +} + +JSClassCallback JSClassEntry::getGetterCallback() const noexcept { + return _getterCallback; +} + +JSClassCallback JSClassEntry::getSetterCallback() const noexcept { + return _setterCallback; +} + +bool JSClassEntry::isWritable() const noexcept { + return _writable; +} + +bool JSClassEntry::isEnumerable() const noexcept { + return _enumerable; +} + +bool JSClassEntry::isConfigurable() const noexcept { + return _configurable; +} + +bool JSClassEntry::isClassMember() const noexcept { + return _classMember; +} + +void JSClassEntry::setClassMember(bool classMember) noexcept { + _classMember = classMember; +} + +JSClassDefinition::JSClassDefinition(const StringBox& name, JSClassConstructorCallback constructor) + : _name(name), _constructor(constructor) {} + +JSClassDefinition::~JSClassDefinition() = default; + +const StringBox& JSClassDefinition::getName() const noexcept { + return _name; +} + +JSClassConstructorCallback JSClassDefinition::getConstructor() const noexcept { + return _constructor; +} + +void JSClassDefinition::setConstructor(JSClassConstructorCallback constructor) noexcept { + _constructor = constructor; +} + +const std::vector& JSClassDefinition::getEntries() const noexcept { + return _entries; +} + +JSClassDefinition& JSClassDefinition::appendInstanceEntry(JSClassEntry entry) { + entry.setClassMember(false); + _entries.emplace_back(std::move(entry)); + return *this; +} + +JSClassDefinition& JSClassDefinition::appendClassEntry(JSClassEntry entry) { + entry.setClassMember(true); + _entries.emplace_back(std::move(entry)); + return *this; +} + +JSClassDefinition& JSClassDefinition::appendMethod(const StringBox& name, JSClassCallback callback) { + return appendMethod(name, callback, true, false, true); +} + +JSClassDefinition& JSClassDefinition::appendMethod( + const StringBox& name, JSClassCallback callback, bool writable, bool enumerable, bool configurable) { + return appendInstanceEntry(JSClassEntry::method(name, callback, writable, enumerable, configurable)); +} + +JSClassDefinition& JSClassDefinition::appendConstant(const StringBox& name, JSValueRef value) { + return appendConstant(name, std::move(value), false, false, false); +} + +JSClassDefinition& JSClassDefinition::appendConstant( + const StringBox& name, JSValueRef value, bool writable, bool enumerable, bool configurable) { + return appendInstanceEntry(JSClassEntry::constant(name, std::move(value), writable, enumerable, configurable)); +} + +JSClassDefinition& JSClassDefinition::appendAccessor(const StringBox& name, + JSClassCallback getter, + JSClassCallback setter) { + return appendAccessor(name, getter, setter, false, true); +} + +JSClassDefinition& JSClassDefinition::appendAccessor( + const StringBox& name, JSClassCallback getter, JSClassCallback setter, bool enumerable, bool configurable) { + return appendInstanceEntry(JSClassEntry::accessor(name, getter, setter, enumerable, configurable)); +} + +JSClassDefinition& JSClassDefinition::appendClassMethod(const StringBox& name, JSClassCallback callback) { + return appendClassMethod(name, callback, true, false, true); +} + +JSClassDefinition& JSClassDefinition::appendClassMethod( + const StringBox& name, JSClassCallback callback, bool writable, bool enumerable, bool configurable) { + return appendClassEntry(JSClassEntry::method(name, callback, writable, enumerable, configurable)); +} + +JSClassDefinition& JSClassDefinition::appendClassConstant(const StringBox& name, JSValueRef value) { + return appendClassConstant(name, std::move(value), false, false, false); +} + +JSClassDefinition& JSClassDefinition::appendClassConstant( + const StringBox& name, JSValueRef value, bool writable, bool enumerable, bool configurable) { + return appendClassEntry(JSClassEntry::constant(name, std::move(value), writable, enumerable, configurable)); +} + +JSClassDefinition& JSClassDefinition::appendClassAccessor(const StringBox& name, + JSClassCallback getter, + JSClassCallback setter) { + return appendClassAccessor(name, getter, setter, false, true); +} + +JSClassDefinition& JSClassDefinition::appendClassAccessor( + const StringBox& name, JSClassCallback getter, JSClassCallback setter, bool enumerable, bool configurable) { + return appendClassEntry(JSClassEntry::accessor(name, getter, setter, enumerable, configurable)); +} + +} // namespace Valdi diff --git a/valdi/src/valdi/runtime/JavaScript/JSClassDefinition.hpp b/valdi/src/valdi/runtime/JavaScript/JSClassDefinition.hpp new file mode 100644 index 00000000..755780a3 --- /dev/null +++ b/valdi/src/valdi/runtime/JavaScript/JSClassDefinition.hpp @@ -0,0 +1,108 @@ +// +// JSClassDefinition.hpp +// ValdiRuntime +// + +#pragma once + +#include "valdi/runtime/JavaScript/JavaScriptTypes.hpp" +#include "valdi_core/cpp/Utils/StringBox.hpp" + +#include + +namespace Valdi { + +class JSFunctionNativeCallContext; + +// The JavaScript instance being constructed is available through callContext.getThisValue(). +// The returned native object is attached to that instance after this callback completes. +using JSClassConstructorCallback = Ref (*)(RefCountable* classOpaque, + JSFunctionNativeCallContext&) noexcept; +using JSClassCallback = JSValueRef (*)(RefCountable* opaque, JSFunctionNativeCallContext&) noexcept; + +enum class JSClassEntryKind { + Method, + Constant, + Accessor, +}; + +class JSClassEntry { +public: + JSClassEntry(); + ~JSClassEntry(); + + static JSClassEntry method(const StringBox& name, JSClassCallback callback); + static JSClassEntry method( + const StringBox& name, JSClassCallback callback, bool writable, bool enumerable, bool configurable); + static JSClassEntry constant(const StringBox& name, JSValueRef value); + static JSClassEntry constant( + const StringBox& name, JSValueRef value, bool writable, bool enumerable, bool configurable); + static JSClassEntry accessor(const StringBox& name, JSClassCallback getter, JSClassCallback setter); + static JSClassEntry accessor( + const StringBox& name, JSClassCallback getter, JSClassCallback setter, bool enumerable, bool configurable); + + const StringBox& getName() const noexcept; + JSClassEntryKind getKind() const noexcept; + const JSValueRef& getValue() const noexcept; + JSClassCallback getMethodCallback() const noexcept; + JSClassCallback getGetterCallback() const noexcept; + JSClassCallback getSetterCallback() const noexcept; + bool isWritable() const noexcept; + bool isEnumerable() const noexcept; + bool isConfigurable() const noexcept; + bool isClassMember() const noexcept; + void setClassMember(bool classMember) noexcept; + +private: + StringBox _name; + JSClassEntryKind _kind = JSClassEntryKind::Method; + JSValueRef _value; + JSClassCallback _methodCallback = nullptr; + JSClassCallback _getterCallback = nullptr; + JSClassCallback _setterCallback = nullptr; + bool _writable = true; + bool _enumerable = false; + bool _configurable = true; + bool _classMember = false; +}; + +class JSClassDefinition { +public: + JSClassDefinition(const StringBox& name, JSClassConstructorCallback constructor); + ~JSClassDefinition(); + + const StringBox& getName() const noexcept; + JSClassConstructorCallback getConstructor() const noexcept; + void setConstructor(JSClassConstructorCallback constructor) noexcept; + const std::vector& getEntries() const noexcept; + + JSClassDefinition& appendInstanceEntry(JSClassEntry entry); + JSClassDefinition& appendClassEntry(JSClassEntry entry); + + JSClassDefinition& appendMethod(const StringBox& name, JSClassCallback callback); + JSClassDefinition& appendMethod( + const StringBox& name, JSClassCallback callback, bool writable, bool enumerable, bool configurable); + JSClassDefinition& appendConstant(const StringBox& name, JSValueRef value); + JSClassDefinition& appendConstant( + const StringBox& name, JSValueRef value, bool writable, bool enumerable, bool configurable); + JSClassDefinition& appendAccessor(const StringBox& name, JSClassCallback getter, JSClassCallback setter); + JSClassDefinition& appendAccessor( + const StringBox& name, JSClassCallback getter, JSClassCallback setter, bool enumerable, bool configurable); + + JSClassDefinition& appendClassMethod(const StringBox& name, JSClassCallback callback); + JSClassDefinition& appendClassMethod( + const StringBox& name, JSClassCallback callback, bool writable, bool enumerable, bool configurable); + JSClassDefinition& appendClassConstant(const StringBox& name, JSValueRef value); + JSClassDefinition& appendClassConstant( + const StringBox& name, JSValueRef value, bool writable, bool enumerable, bool configurable); + JSClassDefinition& appendClassAccessor(const StringBox& name, JSClassCallback getter, JSClassCallback setter); + JSClassDefinition& appendClassAccessor( + const StringBox& name, JSClassCallback getter, JSClassCallback setter, bool enumerable, bool configurable); + +private: + StringBox _name; + JSClassConstructorCallback _constructor; + std::vector _entries; +}; + +} // namespace Valdi diff --git a/valdi/src/valdi/runtime/JavaScript/JSNativeClassBinder.hpp b/valdi/src/valdi/runtime/JavaScript/JSNativeClassBinder.hpp new file mode 100644 index 00000000..bdbc2d21 --- /dev/null +++ b/valdi/src/valdi/runtime/JavaScript/JSNativeClassBinder.hpp @@ -0,0 +1,638 @@ +// +// JSNativeClassBinder.hpp +// ValdiRuntime +// + +#pragma once + +#include "utils/debugging/Assert.hpp" +#include "valdi/runtime/JavaScript/JSClassDefinition.hpp" +#include "valdi/runtime/JavaScript/JavaScriptFunctionCallContext.hpp" +#include "valdi/runtime/JavaScript/JavaScriptUtils.hpp" + +#include +#include +#include +#include + +namespace Valdi { + +namespace JSNativeClassBinderDetail { + +template +using RemoveCVRef = std::remove_cv_t>; + +template +inline constexpr bool kDependentFalse = false; + +template +struct OptionalTraits { + static constexpr bool kIsOptional = false; + using ValueType = void; +}; + +template +struct OptionalTraits> { + static constexpr bool kIsOptional = true; + using ValueType = T; +}; + +template +inline constexpr bool kIsOptionalPrimitive = + std::is_same_v || std::is_same_v || std::is_same_v; + +template +inline constexpr bool kIsSupportedOptional = + OptionalTraits::kIsOptional && kIsOptionalPrimitive::ValueType>; + +template +inline constexpr bool kIsRequiredPrimitive = kIsOptionalPrimitive || std::is_same_v; + +template +inline constexpr bool kIsSupportedParameter = + std::is_same_v || std::is_same_v || std::is_same_v> || + kIsRequiredPrimitive || std::is_same_v || std::is_same_v || + std::is_same_v> || std::is_same_v || kIsSupportedOptional; + +template +inline constexpr bool kIsSupportedResult = + std::is_void_v || std::is_same_v || std::is_same_v || + std::is_same_v> || kIsRequiredPrimitive || std::is_same_v || + std::is_same_v> || std::is_same_v || kIsSupportedOptional; + +template +inline constexpr bool kIsSupportedParameterForm = + !std::is_rvalue_reference_v && (!std::is_lvalue_reference_v || std::is_const_v>); + +template +struct MemberFunctionTraits { + static constexpr bool kIsValid = false; + using ClassType = void; + using ReturnType = void; + using Arguments = std::tuple<>; +}; + +template +struct MemberFunctionTraits { + static constexpr bool kIsValid = true; + using ClassType = Class; + using ReturnType = Return; + using Arguments = std::tuple; +}; + +template +struct MemberFunctionTraits : MemberFunctionTraits {}; + +template +struct MemberFunctionTraits : MemberFunctionTraits {}; + +template +struct MemberFunctionTraits + : MemberFunctionTraits {}; + +template +struct FunctionTraits { + static constexpr bool kIsValid = false; + using ReturnType = void; + using Arguments = std::tuple<>; +}; + +template +struct FunctionTraits { + static constexpr bool kIsValid = true; + using ReturnType = Return; + using Arguments = std::tuple; +}; + +template +struct FunctionTraits : FunctionTraits {}; + +template +struct LastTupleType { + using Type = void; +}; + +template +struct LastTupleType> { + using Type = std::tuple_element_t>; +}; + +template +struct TupleSelect; + +template +struct TupleSelect> { + using Type = std::tuple...>; +}; + +template +using TuplePrefix = typename TupleSelect>::Type; + +template +struct ParsedTuple; + +template +struct ParsedTuple> { + using Type = std::tuple...>; +}; + +template +constexpr bool hasSupportedParameters(std::index_sequence /*unused*/) { + return (kIsSupportedParameter>> && ...) && + (kIsSupportedParameterForm> && ...); +} + +template +struct BindingTraits { + using ReturnType = typename Traits::ReturnType; + using Arguments = typename Traits::Arguments; + + static constexpr size_t kArgumentCount = std::tuple_size_v; + static constexpr size_t kNativeArgumentCount = kArgumentCount == 0 ? 0 : kArgumentCount - 1; + + using LastArgument = typename LastTupleType::Type; + using NativeArguments = TuplePrefix; + using ParsedArguments = typename ParsedTuple::Type; + + static constexpr bool kHasCallContext = std::is_same_v; + static constexpr bool kHasSupportedParameters = + hasSupportedParameters(std::make_index_sequence{}); + static constexpr bool kHasSupportedResult = + !std::is_reference_v && kIsSupportedResult>; +}; + +inline bool isParameterNullOrUndefined(JSFunctionNativeCallContext& callContext, size_t index) { + if (index >= callContext.getParameterSize()) { + return true; + } + + auto value = callContext.getParameter(index); + return callContext.getContext().isValueNull(value) || callContext.getContext().isValueUndefined(value); +} + +template +inline T parsePrimitive(JSFunctionNativeCallContext& callContext, size_t index) { + if constexpr (std::is_same_v) { + return callContext.getParameterAsDouble(index); + } else if constexpr (std::is_same_v) { + return callContext.getParameterAsInt(index); + } else if constexpr (std::is_same_v) { + return callContext.getParameterAsLong(index); + } else if constexpr (std::is_same_v) { + return callContext.getParameterAsBool(index); + } else { + static_assert(kDependentFalse, "Unsupported native class primitive parameter"); + } +} + +template +inline RemoveCVRef parseArgument(JSFunctionNativeCallContext& callContext, size_t index) { + using T = RemoveCVRef; + static_assert(kIsSupportedParameter, "Unsupported native class parameter type"); + static_assert(kIsSupportedParameterForm, + "Native class parameters must be passed by value or const reference"); + + if constexpr (kIsRequiredPrimitive) { + if (isParameterNullOrUndefined(callContext, index)) { + callContext.getExceptionTracker().onError("Native class primitive argument cannot be null or undefined"); + return T(); + } + return parsePrimitive(callContext, index); + } else if constexpr (kIsSupportedOptional) { + if (isParameterNullOrUndefined(callContext, index)) { + return std::nullopt; + } + using ValueType = typename OptionalTraits::ValueType; + return T(parsePrimitive(callContext, index)); + } else if constexpr (std::is_same_v) { + return callContext.getParameterAsValue(index); + } else if constexpr (std::is_same_v) { + return callContext.getParameter(index); + } else { + if (isParameterNullOrUndefined(callContext, index)) { + return T(); + } + + if constexpr (std::is_same_v) { + return callContext.getParameterAsString(index); + } else if constexpr (std::is_same_v>) { + return callContext.getParameterAsStaticString(index); + } else if constexpr (std::is_same_v) { + return callContext.getParameterAsTypedArray(index); + } else if constexpr (std::is_same_v) { + return callContext.getParameterAsBytesView(index); + } else if constexpr (std::is_same_v>) { + return callContext.getParameterAsWrappedObject(index); + } else { + static_assert(kDependentFalse, "Unsupported nullable native class parameter"); + } + } +} + +template +inline bool parseArguments(JSFunctionNativeCallContext& callContext, ParsedArguments& parsedArguments) { + if constexpr (index == std::tuple_size_v) { + return true; + } else { + using Argument = std::tuple_element_t; + std::get(parsedArguments) = parseArgument(callContext, index); + if (!callContext.getExceptionTracker()) { + return false; + } + return parseArguments(callContext, parsedArguments); + } +} + +template +inline decltype(auto) forwardArgument(Parsed& parsed) { + if constexpr (std::is_lvalue_reference_v) { + return static_cast(parsed); + } else { + return static_cast&&>(parsed); + } +} + +template +inline JSValueRef convertResult(Result&& result, JSFunctionNativeCallContext& callContext) { + using T = RemoveCVRef; + static_assert(!std::is_reference_v, "Native class results must be returned by value"); + static_assert(kIsSupportedResult, "Unsupported native class result type"); + + auto& context = callContext.getContext(); + auto& exceptionTracker = callContext.getExceptionTracker(); + + if constexpr (std::is_same_v) { + return valueToJSValue( + context, result, ReferenceInfoBuilder(callContext.getReferenceInfo()).withReturnValue(), exceptionTracker); + } else if constexpr (std::is_same_v) { + if (result.isNull()) { + return context.newUndefined(); + } + return context.newStringUTF8(result.toStringView(), exceptionTracker); + } else if constexpr (std::is_same_v>) { + if (result == nullptr) { + return context.newUndefined(); + } + + return context.newString(*result, exceptionTracker); + } else if constexpr (std::is_same_v || std::is_same_v) { + return context.newNumber(result); + } else if constexpr (std::is_same_v) { + return context.newLong(result, exceptionTracker); + } else if constexpr (std::is_same_v) { + return context.newBool(result); + } else if constexpr (std::is_same_v) { + if (result.getSource() == nullptr) { + return context.newUndefined(); + } + return newTypedArrayFromBytesView(context, TypedArrayType::Uint8Array, result, exceptionTracker); + } else if constexpr (std::is_same_v>) { + if (result == nullptr) { + return context.newUndefined(); + } + return context.newWrappedObject(result, exceptionTracker); + } else if constexpr (std::is_same_v) { + return std::forward(result); + } else if constexpr (kIsSupportedOptional) { + if (!result.has_value()) { + return context.newUndefined(); + } + return convertResult(std::move(result.value()), callContext); + } else { + static_assert(kDependentFalse, "Unsupported native class result"); + } +} + +template +inline decltype(auto) invokeMember(Receiver& receiver, + ParsedArguments& parsedArguments, + JSFunctionNativeCallContext& callContext, + std::index_sequence /*unused*/) { + return (receiver.*method)( + forwardArgument>(std::get(parsedArguments))..., callContext); +} + +template +inline decltype(auto) invokeFunction(ParsedArguments& parsedArguments, + JSFunctionNativeCallContext& callContext, + std::index_sequence /*unused*/) { + return function(forwardArgument>(std::get(parsedArguments))..., + callContext); +} + +template +inline Ref construct(ParsedArguments& parsedArguments, + JSFunctionNativeCallContext& callContext, + std::index_sequence /*unused*/) { + return makeShared( + forwardArgument>(std::get(parsedArguments))..., callContext); +} + +template +struct InstanceCallback { + using CallableTraits = MemberFunctionTraits; + using Binding = BindingTraits; + using ReturnType = typename Binding::ReturnType; + + static constexpr size_t kNativeArgumentCount = Binding::kNativeArgumentCount; + static constexpr bool kIsValid = CallableTraits::kIsValid && Binding::kHasCallContext && + Binding::kHasSupportedParameters && Binding::kHasSupportedResult && + std::is_base_of_v; + + static JSValueRef call(RefCountable* opaque, JSFunctionNativeCallContext& callContext) noexcept { + static_assert(kIsValid, "Invalid native class instance method signature"); + + typename Binding::ParsedArguments parsedArguments; + if (!parseArguments(callContext, parsedArguments)) { + return callContext.getContext().newUndefined(); + } + + auto& receiver = *static_cast(opaque); + auto indices = std::make_index_sequence{}; + if constexpr (std::is_void_v) { + invokeMember(receiver, parsedArguments, callContext, indices); + return callContext.getContext().newUndefined(); + } else { + auto result = invokeMember( + receiver, parsedArguments, callContext, indices); + return convertResult(std::move(result), callContext); + } + } +}; + +template +struct ClassCallback { + using CallableTraits = FunctionTraits; + using Binding = BindingTraits; + using ReturnType = typename Binding::ReturnType; + + static constexpr size_t kNativeArgumentCount = Binding::kNativeArgumentCount; + static constexpr bool kIsValid = CallableTraits::kIsValid && Binding::kHasCallContext && + Binding::kHasSupportedParameters && Binding::kHasSupportedResult; + + static JSValueRef call(RefCountable* /*classOpaque*/, JSFunctionNativeCallContext& callContext) noexcept { + static_assert(kIsValid, "Invalid native class method signature"); + + typename Binding::ParsedArguments parsedArguments; + if (!parseArguments(callContext, parsedArguments)) { + return callContext.getContext().newUndefined(); + } + + auto indices = std::make_index_sequence{}; + if constexpr (std::is_void_v) { + invokeFunction(parsedArguments, callContext, indices); + return callContext.getContext().newUndefined(); + } else { + auto result = + invokeFunction(parsedArguments, callContext, indices); + return convertResult(std::move(result), callContext); + } + } +}; + +template +struct ConstructorCallback { + using NativeArguments = std::tuple; + using ParsedArguments = typename ParsedTuple::Type; + + static constexpr bool kHasSupportedParameters = + hasSupportedParameters(std::index_sequence_for{}); + + static Ref call(RefCountable* /*classOpaque*/, JSFunctionNativeCallContext& callContext) noexcept { + static_assert(std::is_base_of_v, "Native class instance types must inherit from RefCountable"); + static_assert(kHasSupportedParameters, "Invalid native class constructor parameter type"); + static_assert(std::is_constructible_v, + "Native class constructor must accept the bound arguments followed by " + "JSFunctionNativeCallContext&"); + + ParsedArguments parsedArguments; + if (!parseArguments(callContext, parsedArguments)) { + return nullptr; + } + + return construct(parsedArguments, callContext, std::index_sequence_for{}); + } +}; + +} // namespace JSNativeClassBinderDetail + +/** + Binds native C++ constructors and functions to a JSClassDefinition without + storing callable state. JSValue parameters are borrowed for the duration of + the native call. + */ +template +class JSNativeClassBinder { +public: + explicit JSNativeClassBinder(const char* name) : _classDefinition(StringBox::fromCString(name), nullptr) { + static_assert(std::is_base_of_v, "Native class instance types must inherit from RefCountable"); + } + + ~JSNativeClassBinder() = default; + + template + JSNativeClassBinder& bindConstructor() { + SC_ASSERT(_classDefinition.getConstructor() == nullptr); + _classDefinition.setConstructor(&JSNativeClassBinderDetail::ConstructorCallback::call); + return *this; + } + + template + JSNativeClassBinder& bindMethod(const char* name) { + return bindMethod(name, true, false, true); + } + + template + JSNativeClassBinder& bindMethod(const char* name, bool writable, bool enumerable, bool configurable) { + using Callback = JSNativeClassBinderDetail::InstanceCallback; + static_assert(Callback::kIsValid, "Invalid native class instance method signature"); + return appendInstanceEntry( + JSClassEntry::method(makeName(name), &Callback::call, writable, enumerable, configurable)); + } + + template + JSNativeClassBinder& bindAccessor(const char* name) { + return bindAccessor(name, false, true); + } + + template + JSNativeClassBinder& bindAccessor(const char* name, bool enumerable, bool configurable) { + validateInstanceGetter(); + validateInstanceSetter(); + return appendInstanceEntry(JSClassEntry::accessor(makeName(name), + &JSNativeClassBinderDetail::InstanceCallback::call, + &JSNativeClassBinderDetail::InstanceCallback::call, + enumerable, + configurable)); + } + + template + JSNativeClassBinder& bindGetter(const char* name) { + return bindGetter(name, false, true); + } + + template + JSNativeClassBinder& bindGetter(const char* name, bool enumerable, bool configurable) { + validateInstanceGetter(); + return appendInstanceEntry(JSClassEntry::accessor(makeName(name), + &JSNativeClassBinderDetail::InstanceCallback::call, + nullptr, + enumerable, + configurable)); + } + + template + JSNativeClassBinder& bindSetter(const char* name) { + return bindSetter(name, false, true); + } + + template + JSNativeClassBinder& bindSetter(const char* name, bool enumerable, bool configurable) { + validateInstanceSetter(); + return appendInstanceEntry(JSClassEntry::accessor(makeName(name), + nullptr, + &JSNativeClassBinderDetail::InstanceCallback::call, + enumerable, + configurable)); + } + + JSNativeClassBinder& bindConstant(const char* name, JSValueRef value) { + return bindConstant(name, std::move(value), false, false, false); + } + + JSNativeClassBinder& bindConstant( + const char* name, JSValueRef value, bool writable, bool enumerable, bool configurable) { + return appendInstanceEntry( + JSClassEntry::constant(makeName(name), std::move(value), writable, enumerable, configurable)); + } + + template + JSNativeClassBinder& bindClassMethod(const char* name) { + return bindClassMethod(name, true, false, true); + } + + template + JSNativeClassBinder& bindClassMethod(const char* name, bool writable, bool enumerable, bool configurable) { + using Callback = JSNativeClassBinderDetail::ClassCallback; + static_assert(Callback::kIsValid, "Invalid native class method signature"); + return appendClassEntry( + JSClassEntry::method(makeName(name), &Callback::call, writable, enumerable, configurable)); + } + + template + JSNativeClassBinder& bindClassAccessor(const char* name) { + return bindClassAccessor(name, false, true); + } + + template + JSNativeClassBinder& bindClassAccessor(const char* name, bool enumerable, bool configurable) { + validateClassGetter(); + validateClassSetter(); + return appendClassEntry(JSClassEntry::accessor(makeName(name), + &JSNativeClassBinderDetail::ClassCallback::call, + &JSNativeClassBinderDetail::ClassCallback::call, + enumerable, + configurable)); + } + + template + JSNativeClassBinder& bindClassGetter(const char* name) { + return bindClassGetter(name, false, true); + } + + template + JSNativeClassBinder& bindClassGetter(const char* name, bool enumerable, bool configurable) { + validateClassGetter(); + return appendClassEntry(JSClassEntry::accessor(makeName(name), + &JSNativeClassBinderDetail::ClassCallback::call, + nullptr, + enumerable, + configurable)); + } + + template + JSNativeClassBinder& bindClassSetter(const char* name) { + return bindClassSetter(name, false, true); + } + + template + JSNativeClassBinder& bindClassSetter(const char* name, bool enumerable, bool configurable) { + validateClassSetter(); + return appendClassEntry(JSClassEntry::accessor(makeName(name), + nullptr, + &JSNativeClassBinderDetail::ClassCallback::call, + enumerable, + configurable)); + } + + JSNativeClassBinder& bindClassConstant(const char* name, JSValueRef value) { + return bindClassConstant(name, std::move(value), false, false, false); + } + + JSNativeClassBinder& bindClassConstant( + const char* name, JSValueRef value, bool writable, bool enumerable, bool configurable) { + return appendClassEntry( + JSClassEntry::constant(makeName(name), std::move(value), writable, enumerable, configurable)); + } + + const JSClassDefinition& getClassDefinition() & { + return _classDefinition; + } + + JSClassDefinition extractClassDefinition() { + return std::move(_classDefinition); + } + +private: + JSClassDefinition _classDefinition; + + static StringBox makeName(const char* name) { + return StringBox::fromCString(name); + } + + JSNativeClassBinder& appendInstanceEntry(JSClassEntry entry) { + _classDefinition.appendInstanceEntry(std::move(entry)); + return *this; + } + + JSNativeClassBinder& appendClassEntry(JSClassEntry entry) { + _classDefinition.appendClassEntry(std::move(entry)); + return *this; + } + + template + static void validateInstanceGetter() { + using Callback = JSNativeClassBinderDetail::InstanceCallback; + static_assert(Callback::kIsValid, "Invalid native class instance getter signature"); + static_assert(Callback::kNativeArgumentCount == 0, "Native class getters cannot have JavaScript parameters"); + static_assert(!std::is_void_v, "Native class getters must return a value"); + } + + template + static void validateInstanceSetter() { + using Callback = JSNativeClassBinderDetail::InstanceCallback; + static_assert(Callback::kIsValid, "Invalid native class instance setter signature"); + static_assert(Callback::kNativeArgumentCount == 1, + "Native class setters must have exactly one JavaScript parameter"); + static_assert(std::is_void_v, "Native class setters must return void"); + } + + template + static void validateClassGetter() { + using Callback = JSNativeClassBinderDetail::ClassCallback; + static_assert(Callback::kIsValid, "Invalid native class getter signature"); + static_assert(Callback::kNativeArgumentCount == 0, "Native class getters cannot have JavaScript parameters"); + static_assert(!std::is_void_v, "Native class getters must return a value"); + } + + template + static void validateClassSetter() { + using Callback = JSNativeClassBinderDetail::ClassCallback; + static_assert(Callback::kIsValid, "Invalid native class setter signature"); + static_assert(Callback::kNativeArgumentCount == 1, + "Native class setters must have exactly one JavaScript parameter"); + static_assert(std::is_void_v, "Native class setters must return void"); + } +}; + +} // namespace Valdi diff --git a/valdi/src/valdi/runtime/JavaScript/JSNativeClassData.cpp b/valdi/src/valdi/runtime/JavaScript/JSNativeClassData.cpp new file mode 100644 index 00000000..82a7b924 --- /dev/null +++ b/valdi/src/valdi/runtime/JavaScript/JSNativeClassData.cpp @@ -0,0 +1,61 @@ +// +// JSNativeClassData.cpp +// ValdiRuntime +// + +#include "valdi/runtime/JavaScript/JSNativeClassData.hpp" + +#include + +namespace Valdi { + +JSNativeClassData::JSNativeClassData(StringBox name, + const Ref& opaque, + JSClassConstructorCallback constructor) + : _name(std::move(name)), + _opaque(opaque), + _constructor(constructor), + _constructorReferenceInfo(ReferenceInfoBuilder().withObject(_name).asFunction().build()) {} + +JSNativeClassData::~JSNativeClassData() = default; + +const StringBox& JSNativeClassData::getName() const noexcept { + return _name; +} + +const Ref& JSNativeClassData::getOpaque() const noexcept { + return _opaque; +} + +JSClassConstructorCallback JSNativeClassData::getConstructor() const noexcept { + return _constructor; +} + +const ReferenceInfo& JSNativeClassData::getConstructorReferenceInfo() const noexcept { + return _constructorReferenceInfo; +} + +ReferenceInfo JSNativeClassData::makeMemberReferenceInfo(const StringBox& propertyName) const { + return ReferenceInfoBuilder().withObject(_name).withProperty(propertyName).asFunction().build(); +} + +JSNativeClassInstanceData::JSNativeClassInstanceData(const Ref& nativeClass, + const Ref& opaque) + : _nativeClass(nativeClass), _opaque(opaque) {} + +JSNativeClassInstanceData::~JSNativeClassInstanceData() = default; + +const Ref& JSNativeClassInstanceData::getNativeClass() const noexcept { + return _nativeClass; +} + +const Ref& JSNativeClassInstanceData::getOpaque() const noexcept { + return _opaque; +} + +Ref unwrapNativeClassInstanceData(const Ref& wrappedObject) { + auto* instanceData = dynamic_cast(wrappedObject.get()); + return instanceData == nullptr ? wrappedObject : instanceData->getOpaque(); +} + +} // namespace Valdi diff --git a/valdi/src/valdi/runtime/JavaScript/JSNativeClassData.hpp b/valdi/src/valdi/runtime/JavaScript/JSNativeClassData.hpp new file mode 100644 index 00000000..2eb2ec23 --- /dev/null +++ b/valdi/src/valdi/runtime/JavaScript/JSNativeClassData.hpp @@ -0,0 +1,47 @@ +// +// JSNativeClassData.hpp +// ValdiRuntime +// + +#pragma once + +#include "valdi/runtime/JavaScript/JSClassDefinition.hpp" +#include "valdi_core/cpp/Utils/ReferenceInfo.hpp" +#include "valdi_core/cpp/Utils/StringBox.hpp" + +namespace Valdi { + +class JSNativeClassData final : public SimpleRefCountable { +public: + JSNativeClassData(StringBox name, const Ref& opaque, JSClassConstructorCallback constructor); + ~JSNativeClassData() override; + + const StringBox& getName() const noexcept; + const Ref& getOpaque() const noexcept; + JSClassConstructorCallback getConstructor() const noexcept; + const ReferenceInfo& getConstructorReferenceInfo() const noexcept; + ReferenceInfo makeMemberReferenceInfo(const StringBox& propertyName) const; + +private: + StringBox _name; + Ref _opaque; + JSClassConstructorCallback _constructor; + ReferenceInfo _constructorReferenceInfo; +}; + +class JSNativeClassInstanceData final : public SimpleRefCountable { +public: + JSNativeClassInstanceData(const Ref& nativeClass, const Ref& opaque); + ~JSNativeClassInstanceData() override; + + const Ref& getNativeClass() const noexcept; + const Ref& getOpaque() const noexcept; + +private: + Ref _nativeClass; + Ref _opaque; +}; + +Ref unwrapNativeClassInstanceData(const Ref& wrappedObject); + +} // namespace Valdi diff --git a/valdi/src/valdi/runtime/JavaScript/JavaScriptFunctionCallContext.cpp b/valdi/src/valdi/runtime/JavaScript/JavaScriptFunctionCallContext.cpp index 49d74ca4..6c1f8819 100644 --- a/valdi/src/valdi/runtime/JavaScript/JavaScriptFunctionCallContext.cpp +++ b/valdi/src/valdi/runtime/JavaScript/JavaScriptFunctionCallContext.cpp @@ -59,6 +59,10 @@ int32_t JSFunctionNativeCallContext::getParameterAsInt(size_t index) { return _context.valueToInt(getParameter(index), _exceptionTracker); } +int64_t JSFunctionNativeCallContext::getParameterAsLong(size_t index) { + return _context.valueToLong(getParameter(index), _exceptionTracker).toInt64(); +} + Ref JSFunctionNativeCallContext::getParameterAsFunction(size_t index) { return jsValueToFunction( _context, getParameter(index), ReferenceInfoBuilder(_referenceInfo).withParameter(index), _exceptionTracker); diff --git a/valdi/src/valdi/runtime/JavaScript/JavaScriptFunctionCallContext.hpp b/valdi/src/valdi/runtime/JavaScript/JavaScriptFunctionCallContext.hpp index 2eb7fe82..7442be46 100644 --- a/valdi/src/valdi/runtime/JavaScript/JavaScriptFunctionCallContext.hpp +++ b/valdi/src/valdi/runtime/JavaScript/JavaScriptFunctionCallContext.hpp @@ -78,6 +78,7 @@ class JSFunctionNativeCallContext : public JSFunctionCallContext { Ref getParameterAsStaticString(size_t index); double getParameterAsDouble(size_t index); int32_t getParameterAsInt(size_t index); + int64_t getParameterAsLong(size_t index); bool getParameterAsBool(size_t index); Ref getParameterAsFunction(size_t index); JSTypedArray getParameterAsTypedArray(size_t index); diff --git a/valdi/src/valdi/runtime/JavaScript/JavaScriptUtils.cpp b/valdi/src/valdi/runtime/JavaScript/JavaScriptUtils.cpp index ff5a084f..b073ffed 100644 --- a/valdi/src/valdi/runtime/JavaScript/JavaScriptUtils.cpp +++ b/valdi/src/valdi/runtime/JavaScript/JavaScriptUtils.cpp @@ -221,21 +221,6 @@ JSValueRef proxyObjectToJSValue(IJavaScriptContext& jsContext, } } -static JSValueRef staticStringTOJSValue(IJavaScriptContext& jsContext, - const StaticString& staticString, - JSExceptionTracker& exceptionTracker) { - switch (staticString.encoding()) { - case StaticString::Encoding::UTF8: - return jsContext.newStringUTF8(staticString.utf8StringView(), exceptionTracker); - case StaticString::Encoding::UTF16: - return jsContext.newStringUTF16(staticString.utf16StringView(), exceptionTracker); - case StaticString::Encoding::UTF32: { - auto storage = staticString.utf8Storage(); - return jsContext.newStringUTF8(storage.toStringView(), exceptionTracker); - } - } -} - JSValueRef valueToJSValue(IJavaScriptContext& jsContext, const Valdi::Value& value, const ReferenceInfoBuilder& referenceInfoBuilder, @@ -244,7 +229,7 @@ JSValueRef valueToJSValue(IJavaScriptContext& jsContext, case ValueType::InternedString: return jsContext.newStringUTF8(value.toStringBox().toStringView(), exceptionTracker); case ValueType::StaticString: - return staticStringTOJSValue(jsContext, *value.getStaticString(), exceptionTracker); + return jsContext.newString(*value.getStaticString(), exceptionTracker); case ValueType::Double: return jsContext.newNumber(value.toDouble()); case ValueType::Int: diff --git a/valdi/src/valdi/runtime/JavaScript/JavaScriptValueDelegate.cpp b/valdi/src/valdi/runtime/JavaScript/JavaScriptValueDelegate.cpp index db390429..17817aba 100644 --- a/valdi/src/valdi/runtime/JavaScript/JavaScriptValueDelegate.cpp +++ b/valdi/src/valdi/runtime/JavaScript/JavaScriptValueDelegate.cpp @@ -587,6 +587,10 @@ JSValueRef JavaScriptValueDelegate::newStringUTF16(std::u16string_view str, Exce return _jsContext->newStringUTF16(str, toJSExceptionTracker(exceptionTracker)); } +JSValueRef JavaScriptValueDelegate::newString(const StaticString& str, ExceptionTracker& exceptionTracker) { + return _jsContext->newString(str, toJSExceptionTracker(exceptionTracker)); +} + JSValueRef JavaScriptValueDelegate::newByteArray(const BytesView& bytes, ExceptionTracker& exceptionTracker) { return newTypedArrayFromBytesView( *_jsContext, TypedArrayType::Uint8Array, bytes, toJSExceptionTracker(exceptionTracker)); diff --git a/valdi/src/valdi/runtime/JavaScript/JavaScriptValueDelegate.hpp b/valdi/src/valdi/runtime/JavaScript/JavaScriptValueDelegate.hpp index e9b9caac..394480f5 100644 --- a/valdi/src/valdi/runtime/JavaScript/JavaScriptValueDelegate.hpp +++ b/valdi/src/valdi/runtime/JavaScript/JavaScriptValueDelegate.hpp @@ -82,6 +82,7 @@ class JavaScriptValueDelegate : public PlatformValueDelegate { JSValueRef newStringUTF8(std::string_view str, ExceptionTracker& exceptionTracker) final; JSValueRef newStringUTF16(std::u16string_view str, ExceptionTracker& exceptionTracker) final; + JSValueRef newString(const StaticString& str, ExceptionTracker& exceptionTracker) final; JSValueRef newByteArray(const BytesView& bytes, ExceptionTracker& exceptionTracker) final; JSValueRef newTypedArray(TypedArrayType arrayType, diff --git a/valdi/src/valdi/runtime/JavaScript/Modules/Base64ModuleFactory.cpp b/valdi/src/valdi/runtime/JavaScript/Modules/Base64ModuleFactory.cpp new file mode 100644 index 00000000..548f4371 --- /dev/null +++ b/valdi/src/valdi/runtime/JavaScript/Modules/Base64ModuleFactory.cpp @@ -0,0 +1,61 @@ +// +// Base64ModuleFactory.cpp +// valdi-ios +// + +#include "valdi/runtime/JavaScript/Modules/Base64ModuleFactory.hpp" + +#include "utils/encoding/Base64Utils.hpp" +#include "valdi/runtime/JavaScript/JSNativeClassBinder.hpp" +#include "valdi/runtime/JavaScript/JavaScriptFunctionCallContext.hpp" +#include "valdi/runtime/JavaScript/JavaScriptTypes.hpp" +#include "valdi_core/cpp/Utils/Bytes.hpp" + +namespace Valdi { + +Base64ModuleFactory::Base64ModuleFactory() = default; +Base64ModuleFactory::~Base64ModuleFactory() = default; + +StringBox Base64ModuleFactory::getModulePath() const { + return STRING_LITERAL("coreutils/src/Base64Native"); +} + +JSValueRef Base64ModuleFactory::encodeToBase64(const JSTypedArray& bytes, + bool urlSafe, + JSFunctionNativeCallContext& callContext) { + auto base64 = snap::utils::encoding::binaryToBase64(reinterpret_cast(bytes.data), bytes.length); + if (urlSafe) { + snap::utils::encoding::base64ToBase64UrlInPlace(base64); + } + return callContext.getContext().newStringUTF8(base64, callContext.getExceptionTracker()); +} + +JSValueRef Base64ModuleFactory::decodeFromBase64(const Ref& base64, + JSFunctionNativeCallContext& callContext) { + auto storage = base64->utf8Storage(); + auto input = std::string_view(storage.data, storage.length); + + auto bytes = makeShared(); + if (!snap::utils::encoding::base64UrlToBinary(input, *bytes)) { + return callContext.throwError(Error("Invalid base64 string")); + } + + auto arrayBuffer = callContext.getContext().newArrayBuffer(BytesView(bytes), callContext.getExceptionTracker()); + CHECK_CALL_CONTEXT(callContext); + + return callContext.getContext().newTypedArrayFromArrayBuffer( + TypedArrayType::Uint8Array, arrayBuffer.get(), callContext.getExceptionTracker()); +} + +JSValueRef Base64ModuleFactory::loadModule(IJavaScriptContext& jsContext, + const ReferenceInfoBuilder& /*referenceInfoBuilder*/, + JSExceptionTracker& exceptionTracker) { + auto definition = JSNativeClassBinder("Base64Native") + .bindClassMethod<&Base64ModuleFactory::encodeToBase64>("encodeToBase64", true, true, true) + .bindClassMethod<&Base64ModuleFactory::decodeFromBase64>("decodeFromBase64", true, true, true) + .extractClassDefinition(); + + return jsContext.newNativeClass(nullptr, definition, exceptionTracker); +} + +} // namespace Valdi diff --git a/valdi/src/valdi/runtime/JavaScript/Modules/Base64ModuleFactory.hpp b/valdi/src/valdi/runtime/JavaScript/Modules/Base64ModuleFactory.hpp new file mode 100644 index 00000000..228ea4ca --- /dev/null +++ b/valdi/src/valdi/runtime/JavaScript/Modules/Base64ModuleFactory.hpp @@ -0,0 +1,27 @@ +// +// Base64ModuleFactory.hpp +// valdi-ios +// + +#pragma once + +#include "valdi/runtime/JavaScript/Modules/JavaScriptModuleFactory.hpp" + +namespace Valdi { + +class Base64ModuleFactory : public JavaScriptModuleFactory { +public: + Base64ModuleFactory(); + ~Base64ModuleFactory() override; + + StringBox getModulePath() const final; + JSValueRef loadModule(IJavaScriptContext& context, + const ReferenceInfoBuilder& referenceInfoBuilder, + JSExceptionTracker& exceptionTracker) override; + +private: + static JSValueRef encodeToBase64(const JSTypedArray& bytes, bool urlSafe, JSFunctionNativeCallContext& callContext); + static JSValueRef decodeFromBase64(const Ref& base64, JSFunctionNativeCallContext& callContext); +}; + +} // namespace Valdi diff --git a/valdi/src/valdi/runtime/JavaScript/Modules/ProtobufModule.cpp b/valdi/src/valdi/runtime/JavaScript/Modules/ProtobufModule.cpp index ea2937ac..2171c396 100644 --- a/valdi/src/valdi/runtime/JavaScript/Modules/ProtobufModule.cpp +++ b/valdi/src/valdi/runtime/JavaScript/Modules/ProtobufModule.cpp @@ -129,16 +129,7 @@ static JSValueRef getProtobufNonRepeatedField(ProtobufArena& arena, const auto* string = field.getString(); if (string != nullptr) { - switch (string->encoding()) { - case StaticString::Encoding::UTF8: - return jsContext.newStringUTF8(string->utf8StringView(), callContext.getExceptionTracker()); - case StaticString::Encoding::UTF16: - return jsContext.newStringUTF16(string->utf16StringView(), callContext.getExceptionTracker()); - case StaticString::Encoding::UTF32: { - auto storage = string->utf8Storage(); - return jsContext.newStringUTF8(storage.toStringView(), callContext.getExceptionTracker()); - } - } + return jsContext.newString(*string, callContext.getExceptionTracker()); } return jsContext.newUndefined(); diff --git a/valdi/src/valdi/runtime/JavaScript/Modules/UnicodeModuleFactory.cpp b/valdi/src/valdi/runtime/JavaScript/Modules/UnicodeModuleFactory.cpp index fc2ff87f..5314f8e3 100644 --- a/valdi/src/valdi/runtime/JavaScript/Modules/UnicodeModuleFactory.cpp +++ b/valdi/src/valdi/runtime/JavaScript/Modules/UnicodeModuleFactory.cpp @@ -7,13 +7,12 @@ #include "valdi/runtime/JavaScript/Modules/UnicodeModuleFactory.hpp" -#include "valdi/runtime/JavaScript/JSFunctionWithMethod.hpp" +#include "valdi/runtime/JavaScript/JSNativeClassBinder.hpp" #include "valdi/runtime/JavaScript/JavaScriptFunctionCallContext.hpp" #include "valdi/runtime/JavaScript/JavaScriptTypes.hpp" #include "valdi/runtime/JavaScript/JavaScriptUtils.hpp" #include "valdi/runtime/Resources/ResourceManager.hpp" -#include "valdi/runtime/JavaScript/JSFunctionWithCallable.hpp" #include "valdi/runtime/Text/Emoji.hpp" #include "valdi_core/cpp/Text/UTF16Utils.hpp" #include "valdi_core/cpp/Utils/ByteBuffer.hpp" @@ -320,20 +319,14 @@ static ProcessedUnicodeString processUnicodeString(const uint32_t* data, return output; } -JSValueRef UnicodeModuleFactory::strToCodepoints(JSFunctionNativeCallContext& callContext) { - auto str = callContext.getParameterAsStaticString(0); - CHECK_CALL_CONTEXT(callContext); - - auto normalize = callContext.getParameterAsBool(1); - CHECK_CALL_CONTEXT(callContext); - - auto includeCategorization = !callContext.getParameterAsBool(2); - CHECK_CALL_CONTEXT(callContext); - +JSValueRef UnicodeModuleFactory::strToCodepoints(const Ref& str, + bool normalize, + bool disableCategorization, + JSFunctionNativeCallContext& callContext) { auto utf32Storage = str->utf32Storage(); auto processedString = processUnicodeString( - reinterpret_cast(utf32Storage.data), utf32Storage.length, normalize, includeCategorization); + reinterpret_cast(utf32Storage.data), utf32Storage.length, normalize, !disableCategorization); auto bufferJs = callContext.getContext().newArrayBuffer(processedString.buffer->toBytesView(), callContext.getExceptionTracker()); @@ -355,20 +348,14 @@ static JSValueRef newStringFromUtf32(JSFunctionNativeCallContext& callContext, callContext.getExceptionTracker()); } -JSValueRef UnicodeModuleFactory::codepointsToStr(JSFunctionNativeCallContext& callContext) { +JSValueRef UnicodeModuleFactory::codepointsToStr(JSValue codepoints, + bool normalize, + bool disableCategorization, + JSFunctionNativeCallContext& callContext) { static auto kStr = STRING_LITERAL("str"); static auto kBuffer = STRING_LITERAL("buffer"); - auto codepointsJs = callContext.getParameter(0); - CHECK_CALL_CONTEXT(callContext); - - auto arrayLength = jsArrayGetLength(callContext.getContext(), codepointsJs, callContext.getExceptionTracker()); - CHECK_CALL_CONTEXT(callContext); - - auto normalize = callContext.getParameterAsBool(1); - CHECK_CALL_CONTEXT(callContext); - - auto includeCategorization = !callContext.getParameterAsBool(2); + auto arrayLength = jsArrayGetLength(callContext.getContext(), codepoints, callContext.getExceptionTracker()); CHECK_CALL_CONTEXT(callContext); auto flagsJs = callContext.getContext().newArray(arrayLength, callContext.getExceptionTracker()); @@ -379,7 +366,7 @@ JSValueRef UnicodeModuleFactory::codepointsToStr(JSFunctionNativeCallContext& ca for (size_t i = 0; i < arrayLength; i++) { auto codepointJs = - callContext.getContext().getObjectPropertyForIndex(codepointsJs, i, callContext.getExceptionTracker()); + callContext.getContext().getObjectPropertyForIndex(codepoints, i, callContext.getExceptionTracker()); CHECK_CALL_CONTEXT(callContext); auto codepoint = callContext.getContext().valueToInt(codepointJs.get(), callContext.getExceptionTracker()); @@ -389,7 +376,7 @@ JSValueRef UnicodeModuleFactory::codepointsToStr(JSFunctionNativeCallContext& ca } auto processedString = - processUnicodeString(allCodepoints.data(), allCodepoints.size(), normalize, includeCategorization); + processUnicodeString(allCodepoints.data(), allCodepoints.size(), normalize, !disableCategorization); auto bufferJs = callContext.getContext().newArrayBuffer(processedString.buffer->toBytesView(), callContext.getExceptionTracker()); @@ -420,13 +407,10 @@ static JSValueRef throwInvalidEncoding(JSFunctionNativeCallContext& callContext) return callContext.throwError(Error("Invalid encoding")); } -JSValueRef UnicodeModuleFactory::encodeString(JSFunctionNativeCallContext& callContext) { - auto str = callContext.getParameterAsStaticString(0); - CHECK_CALL_CONTEXT(callContext); - auto encoding = static_cast(callContext.getParameterAsInt(1)); - CHECK_CALL_CONTEXT(callContext); - - switch (encoding) { +JSValueRef UnicodeModuleFactory::encodeString(const Ref& str, + int32_t encoding, + JSFunctionNativeCallContext& callContext) { + switch (static_cast(encoding)) { case TextEncoding::UTF8: { auto storage = str->utf8Storage(); return callContext.getContext().newArrayBufferCopy( @@ -447,13 +431,10 @@ JSValueRef UnicodeModuleFactory::encodeString(JSFunctionNativeCallContext& callC } } -JSValueRef UnicodeModuleFactory::decodeIntoString(JSFunctionNativeCallContext& callContext) { - auto buffer = callContext.getParameterAsTypedArray(0); - CHECK_CALL_CONTEXT(callContext); - auto encoding = static_cast(callContext.getParameterAsInt(1)); - CHECK_CALL_CONTEXT(callContext); - - switch (encoding) { +JSValueRef UnicodeModuleFactory::decodeIntoString(const JSTypedArray& buffer, + int32_t encoding, + JSFunctionNativeCallContext& callContext) { + switch (static_cast(encoding)) { case TextEncoding::UTF8: return callContext.getContext().newStringUTF8( std::string_view(reinterpret_cast(buffer.data), buffer.length), @@ -473,38 +454,17 @@ JSValueRef UnicodeModuleFactory::decodeIntoString(JSFunctionNativeCallContext& c } JSValueRef UnicodeModuleFactory::loadModule(IJavaScriptContext& jsContext, - const ReferenceInfoBuilder& referenceInfoBuilder, + const ReferenceInfoBuilder& /*referenceInfoBuilder*/, JSExceptionTracker& exceptionTracker) { - auto module = jsContext.newObject(exceptionTracker); - if (!exceptionTracker) { - return JSValueRef(); - } - - auto functions = std::vector({ - std::make_pair("strToCodepoints", &UnicodeModuleFactory::strToCodepoints), - std::make_pair("codepointsToStr", &UnicodeModuleFactory::codepointsToStr), - std::make_pair("encodeString", &UnicodeModuleFactory::encodeString), - std::make_pair("decodeIntoString", &UnicodeModuleFactory::decodeIntoString), - }); - - for (const auto& function : functions) { - auto functionName = StringCache::getGlobal().makeString(std::string_view(function.first)); - - auto jsFunction = jsContext.newFunction( - makeShared(ReferenceInfoBuilder().withProperty(functionName), function.second), - exceptionTracker); - if (!exceptionTracker) { - return JSValueRef(); - } - - jsContext.setObjectProperty(module.get(), std::string_view(function.first), jsFunction.get(), exceptionTracker); - - if (!exceptionTracker) { - return JSValueRef(); - } - } - - return module; + auto definition = + JSNativeClassBinder("UnicodeNative") + .bindClassMethod<&UnicodeModuleFactory::strToCodepoints>("strToCodepoints", true, true, true) + .bindClassMethod<&UnicodeModuleFactory::codepointsToStr>("codepointsToStr", true, true, true) + .bindClassMethod<&UnicodeModuleFactory::encodeString>("encodeString", true, true, true) + .bindClassMethod<&UnicodeModuleFactory::decodeIntoString>("decodeIntoString", true, true, true) + .extractClassDefinition(); + + return jsContext.newNativeClass(nullptr, definition, exceptionTracker); } } // namespace Valdi diff --git a/valdi/src/valdi/runtime/JavaScript/Modules/UnicodeModuleFactory.hpp b/valdi/src/valdi/runtime/JavaScript/Modules/UnicodeModuleFactory.hpp index fa3676b2..202b5547 100644 --- a/valdi/src/valdi/runtime/JavaScript/Modules/UnicodeModuleFactory.hpp +++ b/valdi/src/valdi/runtime/JavaScript/Modules/UnicodeModuleFactory.hpp @@ -24,10 +24,20 @@ class UnicodeModuleFactory : public JavaScriptModuleFactory { JSExceptionTracker& exceptionTracker) override; private: - static JSValueRef strToCodepoints(JSFunctionNativeCallContext& callContext); - static JSValueRef codepointsToStr(JSFunctionNativeCallContext& callContext); - static JSValueRef encodeString(JSFunctionNativeCallContext& callContext); - static JSValueRef decodeIntoString(JSFunctionNativeCallContext& callContext); + static JSValueRef strToCodepoints(const Ref& str, + bool normalize, + bool disableCategorization, + JSFunctionNativeCallContext& callContext); + static JSValueRef codepointsToStr(JSValue codepoints, + bool normalize, + bool disableCategorization, + JSFunctionNativeCallContext& callContext); + static JSValueRef encodeString(const Ref& str, + int32_t encoding, + JSFunctionNativeCallContext& callContext); + static JSValueRef decodeIntoString(const JSTypedArray& buffer, + int32_t encoding, + JSFunctionNativeCallContext& callContext); }; } // namespace Valdi diff --git a/valdi/src/valdi/runtime/Runtime.cpp b/valdi/src/valdi/runtime/Runtime.cpp index abccb844..51d9fa94 100644 --- a/valdi/src/valdi/runtime/Runtime.cpp +++ b/valdi/src/valdi/runtime/Runtime.cpp @@ -31,6 +31,7 @@ #include "valdi_core/cpp/Context/ComponentPath.hpp" #include "valdi/runtime/JavaScript/Modules/AttributedTextNativeModuleFactory.hpp" +#include "valdi/runtime/JavaScript/Modules/Base64ModuleFactory.hpp" #include "valdi/runtime/JavaScript/Modules/FileSystemFactory.hpp" #include "valdi/runtime/JavaScript/Modules/JavaScriptModuleFactoryBridge.hpp" #include "valdi/runtime/JavaScript/Modules/PersistentStoreModuleFactory.hpp" @@ -217,6 +218,7 @@ void Runtime::postInit() { registerJavaScriptModuleFactory(makeShared(*_resourceManager, _workerQueue, *_logger)); registerJavaScriptModuleFactory(makeShared()); + registerJavaScriptModuleFactory(makeShared()); if constexpr (kTCPSocketEnabled) { registerNativeModuleFactory(makeShared().toShared()); diff --git a/valdi/src/valdi/v8/V8JavaScriptContext.cpp b/valdi/src/valdi/v8/V8JavaScriptContext.cpp index d69c524f..b43e4523 100644 --- a/valdi/src/valdi/v8/V8JavaScriptContext.cpp +++ b/valdi/src/valdi/v8/V8JavaScriptContext.cpp @@ -507,6 +507,20 @@ JSValueRef V8JavaScriptContext::newWrappedObject(const Ref& wrappe return toRetainedJSValueRef(IndirectV8Persistent::make(_isolate, val)); } +JSValueRef V8JavaScriptContext::newNativeClass(const Ref& /*classOpaque*/, + const JSClassDefinition& /*classDefinition*/, + JSExceptionTracker& exceptionTracker) { + exceptionTracker.onError("Native classes are not supported by V8"); + return JSValueRef(); +} + +JSValueRef V8JavaScriptContext::newObjectFromNativeClass(const Ref& /*opaque*/, + const JSValue& /*cls*/, + JSExceptionTracker& exceptionTracker) { + exceptionTracker.onError("Native classes are not supported by V8"); + return JSValueRef(); +} + JSValueRef V8JavaScriptContext::newWeakRef(const JSValue& object, JSExceptionTracker& exceptionTracker) { v8::HandleScope handleScope(_isolate); auto indirect = fromValdiJSValueToIndirect(object); diff --git a/valdi/src/valdi/v8/V8JavaScriptContext.hpp b/valdi/src/valdi/v8/V8JavaScriptContext.hpp index fdd22143..cdfb2f17 100644 --- a/valdi/src/valdi/v8/V8JavaScriptContext.hpp +++ b/valdi/src/valdi/v8/V8JavaScriptContext.hpp @@ -60,6 +60,14 @@ class V8JavaScriptContext : public IJavaScriptContext { JSValueRef newWrappedObject(const Ref& wrappedObject, JSExceptionTracker& exceptionTracker) override; + JSValueRef newNativeClass(const Ref& classOpaque, + const JSClassDefinition& classDefinition, + JSExceptionTracker& exceptionTracker) override; + + JSValueRef newObjectFromNativeClass(const Ref& opaque, + const JSValue& cls, + JSExceptionTracker& exceptionTracker) override; + JSValueRef newWeakRef(const JSValue& object, JSExceptionTracker& exceptionTracker) override; JSValueRef getObjectProperty(const JSValue& object, diff --git a/valdi/test/integration/JSIntegrationTests.cpp b/valdi/test/integration/JSIntegrationTests.cpp index ccaa1b52..6a350883 100644 --- a/valdi/test/integration/JSIntegrationTests.cpp +++ b/valdi/test/integration/JSIntegrationTests.cpp @@ -36,6 +36,7 @@ namespace ValdiTest { struct MockJavaScriptContextListener : public IJavaScriptContextListener { std::vector unhandledPromiseResults; + size_t interruptCount = 0; JSValueRef symbolicateError(const JSValueRef& jsError) override { return jsError; @@ -57,7 +58,9 @@ struct MockJavaScriptContextListener : public IJavaScriptContextListener { exceptionTracker.clearError(); } - void onInterrupt(IJavaScriptContext& jsContext) override {} + void onInterrupt(IJavaScriptContext& jsContext) override { + interruptCount++; + } }; struct PropertyNamesVisitor : public IJavaScriptPropertyNamesVisitor { @@ -283,6 +286,29 @@ TEST_P(JSContextFixture, canCreateUTF16String) { ASSERT_EQ(std::string("Hello World"), staticStringResult->toStdString()); } +TEST_P(JSContextFixture, canCreateStringFromStaticString) { + MAIN_THREAD_INIT(); + auto wrapper = createWrapper(); + auto jsEntry = wrapper.makeJsEntry(); + auto& context = jsEntry.context; + auto& exceptionTracker = jsEntry.exceptionTracker; + + auto utf8 = StaticString::makeUTF8("UTF8"); + auto utf8Value = context.newString(*utf8, exceptionTracker); + ASSERT_EQ(STRING_LITERAL("UTF8"), context.valueToString(utf8Value.get(), exceptionTracker)); + + std::u16string utf16 = u"UTF16"; + auto utf16Value = + context.newString(*StaticString::makeUTF16(utf16.data(), utf16.size()), exceptionTracker); + ASSERT_EQ(STRING_LITERAL("UTF16"), context.valueToString(utf16Value.get(), exceptionTracker)); + + std::u32string utf32 = U"UTF32"; + auto utf32Value = + context.newString(*StaticString::makeUTF32(utf32.data(), utf32.size()), exceptionTracker); + ASSERT_EQ(STRING_LITERAL("UTF32"), context.valueToString(utf32Value.get(), exceptionTracker)); + jsEntry.checkException(); +} + TEST_P(JSContextFixture, canCreateBools) { MAIN_THREAD_INIT(); auto wrapper = createWrapper(); @@ -645,6 +671,350 @@ struct DummyObject : public Valdi::ValdiObject { VALDI_CLASS_HEADER_IMPL(DummyObject); }; +class NativeCounter final : public Valdi::SimpleRefCountable { +public: + explicit NativeCounter(int32_t value) : value(value) {} + + int32_t value; +}; + +class NativeCounterClass final : public Valdi::SimpleRefCountable { +public: + int32_t constructorCalls = 0; + int32_t staticValue = 0; + int32_t multiplier = 2; +}; + +static Ref constructNativeCounter(RefCountable* classOpaque, + JSFunctionNativeCallContext& callContext) noexcept { + auto value = callContext.getParameterAsInt(0); + if (!callContext.getExceptionTracker()) { + return nullptr; + } + static_cast(classOpaque)->constructorCalls++; + return makeShared(value); +} + +static JSValueRef addToNativeCounter(RefCountable* opaque, JSFunctionNativeCallContext& callContext) noexcept { + auto* counter = static_cast(opaque); + counter->value += callContext.getParameterAsInt(0); + if (!callContext.getExceptionTracker()) { + return callContext.getContext().newUndefined(); + } + return callContext.getContext().newNumber(counter->value); +} + +static JSValueRef getNativeCounterValue(RefCountable* opaque, JSFunctionNativeCallContext& callContext) noexcept { + return callContext.getContext().newNumber(static_cast(opaque)->value); +} + +static JSValueRef setNativeCounterValue(RefCountable* opaque, + JSFunctionNativeCallContext& callContext) noexcept { + static_cast(opaque)->value = callContext.getParameterAsInt(0); + return callContext.getContext().newUndefined(); +} + +static JSValueRef doubleNativeCounterValue(RefCountable* classOpaque, + JSFunctionNativeCallContext& callContext) noexcept { + auto value = callContext.getParameterAsInt(0); + if (!callContext.getExceptionTracker()) { + return callContext.getContext().newUndefined(); + } + return callContext.getContext().newNumber(value * static_cast(classOpaque)->multiplier); +} + +static JSValueRef getNativeCounterStaticValue(RefCountable* classOpaque, + JSFunctionNativeCallContext& callContext) noexcept { + return callContext.getContext().newNumber(static_cast(classOpaque)->staticValue); +} + +static JSValueRef setNativeCounterStaticValue(RefCountable* classOpaque, + JSFunctionNativeCallContext& callContext) noexcept { + static_cast(classOpaque)->staticValue = callContext.getParameterAsInt(0); + return callContext.getContext().newUndefined(); +} + +static JSClassDefinition makeNativeCounterClassDefinition(IJavaScriptContext& context, + JSExceptionTracker& exceptionTracker, + const StringBox& name, + JSClassConstructorCallback constructor) { + JSClassDefinition definition(name, constructor); + definition + .appendInstanceEntry(JSClassEntry::method(STRING_LITERAL("add"), &addToNativeCounter)) + .appendConstant(STRING_LITERAL("kind"), context.newStringUTF8("native-counter", exceptionTracker)) + .appendAccessor(STRING_LITERAL("value"), &getNativeCounterValue, &setNativeCounterValue) + .appendClassEntry(JSClassEntry::method(STRING_LITERAL("twice"), &doubleNativeCounterValue)) + .appendClassConstant(STRING_LITERAL("category"), + context.newStringUTF8("counter-class", exceptionTracker)) + .appendClassAccessor( + STRING_LITERAL("sharedValue"), &getNativeCounterStaticValue, &setNativeCounterStaticValue); + return definition; +} + +struct NativeCounterTestValues { + Ref classOpaque; + Ref instanceOpaque; + JSValueRef cls; + JSValueRef instance; +}; + +static NativeCounterTestValues setUpNativeCounterTest(JSEntry& jsEntry, + JSClassConstructorCallback constructor) { + auto& context = jsEntry.context; + auto& exceptionTracker = jsEntry.exceptionTracker; + auto classOpaque = makeShared(); + auto classDefinition = + makeNativeCounterClassDefinition(context, exceptionTracker, STRING_LITERAL("NativeCounter"), constructor); + jsEntry.checkException(); + + auto cls = context.newNativeClass(classOpaque, classDefinition, exceptionTracker); + jsEntry.checkException(); + auto instanceOpaque = makeShared(10); + auto instance = context.newObjectFromNativeClass(instanceOpaque, cls.get(), exceptionTracker); + jsEntry.checkException(); + + auto global = context.getGlobalObject(exceptionTracker); + context.setObjectProperty(global.get(), "NativeCounter", cls.get(), exceptionTracker); + context.setObjectProperty(global.get(), "nativeCounter", instance.get(), exceptionTracker); + jsEntry.checkException(); + + return NativeCounterTestValues{ + std::move(classOpaque), + std::move(instanceOpaque), + std::move(cls), + std::move(instance), + }; +} + +static JSValueRef evaluateNativeCounterExpression(JSEntry& jsEntry, const char* source) { + auto value = jsEntry.context.evaluate(source, "NativeClassEntries.js", jsEntry.exceptionTracker); + jsEntry.checkException(); + return value; +} + +TEST_P(JSContextFixture, canCreateAndUseNativeClass) { + MAIN_THREAD_INIT(); + auto wrapper = createWrapper(); + auto jsEntry = wrapper.makeJsEntry(); + auto& context = jsEntry.context; + auto& exceptionTracker = jsEntry.exceptionTracker; + + auto values = setUpNativeCounterTest(jsEntry, &constructNativeCounter); + ASSERT_GT(values.classOpaque.use_count(), 1); + ASSERT_EQ(0, values.classOpaque->constructorCalls); + ASSERT_EQ(values.instanceOpaque, + castOrNull(context.valueToWrappedObject(values.instance.get(), exceptionTracker))); + jsEntry.checkException(); + + ASSERT_TRUE(context.valueToBool( + evaluateNativeCounterExpression(jsEntry, "nativeCounter instanceof NativeCounter").get(), exceptionTracker)); + ASSERT_TRUE(context.valueToBool( + evaluateNativeCounterExpression(jsEntry, "nativeCounter.add === NativeCounter.prototype.add").get(), + exceptionTracker)); + ASSERT_EQ(10, + context.valueToInt( + evaluateNativeCounterExpression(jsEntry, "nativeCounter.value").get(), exceptionTracker)); + ASSERT_EQ(15, + context.valueToInt( + evaluateNativeCounterExpression(jsEntry, "nativeCounter.add(5)").get(), exceptionTracker)); + + evaluateNativeCounterExpression(jsEntry, "nativeCounter.value = 20"); + ASSERT_EQ(20, values.instanceOpaque->value); + ASSERT_EQ(20, + context.valueToInt( + evaluateNativeCounterExpression(jsEntry, "nativeCounter.value").get(), exceptionTracker)); + ASSERT_EQ(STRING_LITERAL("native-counter"), + context.valueToString( + evaluateNativeCounterExpression(jsEntry, "nativeCounter.kind").get(), exceptionTracker)); + + ASSERT_EQ(12, + context.valueToInt( + evaluateNativeCounterExpression(jsEntry, "NativeCounter.twice(6)").get(), exceptionTracker)); + ASSERT_EQ(STRING_LITERAL("counter-class"), + context.valueToString( + evaluateNativeCounterExpression(jsEntry, "NativeCounter.category").get(), exceptionTracker)); + + evaluateNativeCounterExpression(jsEntry, "NativeCounter.sharedValue = 9"); + ASSERT_EQ(9, values.classOpaque->staticValue); + ASSERT_EQ(9, + context.valueToInt( + evaluateNativeCounterExpression(jsEntry, "NativeCounter.sharedValue").get(), exceptionTracker)); + + auto constructed = + context.evaluate("globalThis.constructedNativeCounter = new NativeCounter(4); constructedNativeCounter", + "NativeClassConstructor.js", + exceptionTracker); + jsEntry.checkException(); + ASSERT_EQ(1, values.classOpaque->constructorCalls); + auto constructedCounter = + castOrNull(context.valueToWrappedObject(constructed.get(), exceptionTracker)); + jsEntry.checkException(); + ASSERT_NE(nullptr, constructedCounter); + ASSERT_EQ(4, constructedCounter->value); + + RefCountableAutoreleasePool autoreleasePool; + auto lifetimeCounter = makeShared(1); + { + auto lifetimeObject = context.ensureRetainedValue( + context.newObjectFromNativeClass(lifetimeCounter, values.cls.get(), exceptionTracker)); + jsEntry.checkException(); + autoreleasePool.releaseAll(); + ASSERT_EQ(2, lifetimeCounter.use_count()); + } + context.garbageCollect(); + autoreleasePool.releaseAll(); + ASSERT_EQ(1, lifetimeCounter.use_count()); +} + +TEST_P(JSContextFixture, nativeClassEntriesHaveExpectedDescriptors) { + MAIN_THREAD_INIT(); + auto wrapper = createWrapper(); + auto jsEntry = wrapper.makeJsEntry(); + auto& context = jsEntry.context; + auto& exceptionTracker = jsEntry.exceptionTracker; + setUpNativeCounterTest(jsEntry, nullptr); + + ASSERT_FALSE(context.valueToBool( + evaluateNativeCounterExpression( + jsEntry, "Object.getOwnPropertyDescriptor(NativeCounter.prototype, 'add').enumerable") + .get(), + exceptionTracker)); + ASSERT_TRUE(context.valueToBool( + evaluateNativeCounterExpression( + jsEntry, "Object.getOwnPropertyDescriptor(NativeCounter.prototype, 'add').writable") + .get(), + exceptionTracker)); + ASSERT_TRUE(context.valueToBool( + evaluateNativeCounterExpression( + jsEntry, "Object.getOwnPropertyDescriptor(NativeCounter.prototype, 'add').configurable") + .get(), + exceptionTracker)); + ASSERT_FALSE(context.valueToBool( + evaluateNativeCounterExpression( + jsEntry, "Object.getOwnPropertyDescriptor(NativeCounter.prototype, 'kind').writable") + .get(), + exceptionTracker)); + ASSERT_FALSE(context.valueToBool( + evaluateNativeCounterExpression( + jsEntry, "Object.getOwnPropertyDescriptor(NativeCounter.prototype, 'kind').configurable") + .get(), + exceptionTracker)); + ASSERT_FALSE(context.valueToBool( + evaluateNativeCounterExpression( + jsEntry, "Object.getOwnPropertyDescriptor(NativeCounter.prototype, 'value').enumerable") + .get(), + exceptionTracker)); + ASSERT_TRUE(context.valueToBool( + evaluateNativeCounterExpression( + jsEntry, "Object.getOwnPropertyDescriptor(NativeCounter.prototype, 'value').configurable") + .get(), + exceptionTracker)); + jsEntry.checkException(); +} + +TEST_P(JSContextFixture, nativeClassMethodsCannotBeExportedAsNativeFunctions) { + MAIN_THREAD_INIT(); + auto wrapper = createWrapper(); + auto jsEntry = wrapper.makeJsEntry(); + auto& context = jsEntry.context; + auto& exceptionTracker = jsEntry.exceptionTracker; + setUpNativeCounterTest(jsEntry, nullptr); + + auto method = evaluateNativeCounterExpression(jsEntry, "NativeCounter.prototype.add"); + ASSERT_EQ(nullptr, context.valueToFunction(method.get(), exceptionTracker)); + jsEntry.checkException(); +} + +TEST_P(JSContextFixture, nativeClassCallbacksDeliverInterrupts) { + MAIN_THREAD_INIT(); + auto wrapper = createWrapper(); + auto jsEntry = wrapper.makeJsEntry(); + auto& context = jsEntry.context; + auto& exceptionTracker = jsEntry.exceptionTracker; + auto values = setUpNativeCounterTest(jsEntry, &constructNativeCounter); + MockJavaScriptContextListener listener; + context.setListener(&listener); + + auto argument = context.newNumber(1); + + auto instanceMethod = evaluateNativeCounterExpression(jsEntry, "NativeCounter.prototype.add"); + JSFunctionCallContext instanceCallContext(context, &argument, 1, exceptionTracker); + instanceCallContext.setThisValue(values.instance.get()); + context.requestInterrupt(); + context.callObjectAsFunction(instanceMethod.get(), instanceCallContext); + jsEntry.checkException(); + ASSERT_EQ(1, listener.interruptCount); + + auto classMethod = evaluateNativeCounterExpression(jsEntry, "NativeCounter.twice"); + JSFunctionCallContext classCallContext(context, &argument, 1, exceptionTracker); + classCallContext.setThisValue(values.cls.get()); + context.requestInterrupt(); + context.callObjectAsFunction(classMethod.get(), classCallContext); + jsEntry.checkException(); + ASSERT_EQ(2, listener.interruptCount); + + JSFunctionCallContext constructorCallContext(context, &argument, 1, exceptionTracker); + context.requestInterrupt(); + context.callObjectAsConstructor(values.cls.get(), constructorCallContext); + jsEntry.checkException(); + ASSERT_EQ(3, listener.interruptCount); + + context.setListener(nullptr); +} + +TEST_P(JSContextFixture, nativeClassRejectsInvalidUsage) { + MAIN_THREAD_INIT(); + auto wrapper = createWrapper(); + auto jsEntry = wrapper.makeJsEntry(); + auto& context = jsEntry.context; + auto& exceptionTracker = jsEntry.exceptionTracker; + setUpNativeCounterTest(jsEntry, &constructNativeCounter); + + auto otherDefinition = makeNativeCounterClassDefinition( + context, exceptionTracker, STRING_LITERAL("OtherNativeCounter"), nullptr); + auto otherClassOpaque = makeShared(); + auto otherClass = context.newNativeClass(otherClassOpaque, otherDefinition, exceptionTracker); + auto otherCounter = makeShared(3); + auto otherObject = context.newObjectFromNativeClass(otherCounter, otherClass.get(), exceptionTracker); + auto global = context.getGlobalObject(exceptionTracker); + context.setObjectProperty(global.get(), "OtherNativeCounter", otherClass.get(), exceptionTracker); + context.setObjectProperty(global.get(), "otherNativeCounter", otherObject.get(), exceptionTracker); + jsEntry.checkException(); + + ASSERT_TRUE(context.valueToBool( + evaluateNativeCounterExpression( + jsEntry, + "(() => { try { NativeCounter(); return false; } " + "catch (error) { return error instanceof Error; } })()") + .get(), + exceptionTracker)); + // JavaScriptCore's native constructor callback does not expose new.target, so it cannot detect this case. + if (!isJSCore()) { + ASSERT_TRUE(context.valueToBool( + evaluateNativeCounterExpression( + jsEntry, + "(() => { try { class Child extends NativeCounter {}; new Child(1); return false; } " + "catch (error) { return error instanceof Error; } })()") + .get(), + exceptionTracker)); + } + ASSERT_TRUE(context.valueToBool( + evaluateNativeCounterExpression( + jsEntry, + "(() => { try { NativeCounter.prototype.add.call(otherNativeCounter, 1); return false; } " + "catch (error) { return error instanceof Error; } })()") + .get(), + exceptionTracker)); + ASSERT_TRUE(context.valueToBool( + evaluateNativeCounterExpression( + jsEntry, + "(() => { try { new OtherNativeCounter(); return false; } " + "catch (error) { return error instanceof Error; } })()") + .get(), + exceptionTracker)); + jsEntry.checkException(); +} + TEST_P(JSContextFixture, canCreateWrappedObject) { SKIP_IF_V8("Ticket: 2255"); MAIN_THREAD_INIT(); @@ -1828,4 +2198,4 @@ INSTANTIATE_TEST_SUITE_P(JSIntegrationTests, JavaScriptEngineTestCase::Hermes), PrintJavaScriptEngineType()); -} // namespace ValdiTest \ No newline at end of file +} // namespace ValdiTest diff --git a/valdi/test/integration/JSNativeClassBinder_tests.cpp b/valdi/test/integration/JSNativeClassBinder_tests.cpp new file mode 100644 index 00000000..fd92fc7b --- /dev/null +++ b/valdi/test/integration/JSNativeClassBinder_tests.cpp @@ -0,0 +1,445 @@ +#include "JSBridgeTestFixture.hpp" +#include "JSIntegrationTestsUtils.hpp" +#include "valdi/runtime/JavaScript/JSNativeClassBinder.hpp" + +#include + +using namespace Valdi; + +namespace ValdiTest { + +class JSNativeClassBinderTest : public JSBridgeTestFixture { +protected: + JSContextWrapper createWrapper() { + return JSContextWrapper(getJsBridge(), nullptr); + } +}; + +class BinderTestObject final : public SimpleRefCountable { +public: + static constexpr int64_t kLargeLongValue = static_cast(3670116110564327421LL); + + explicit BinderTestObject(int32_t value) : _value(value) {} + + BinderTestObject(int32_t value, JSFunctionNativeCallContext& callContext) + : _value(value), _constructorContext(&callContext.getContext()) { + auto constructedValue = callContext.getContext().newNumber(value); + callContext.getContext().setObjectProperty( + callContext.getThisValue(), "constructedValue", constructedValue.get(), callContext.getExceptionTracker()); + } + + int32_t add(int32_t amount, JSFunctionNativeCallContext& callContext) { + _lastContext = &callContext.getContext(); + _value += amount; + return _value; + } + + void reset(JSFunctionNativeCallContext& callContext) { + _lastContext = &callContext.getContext(); + _value = 0; + } + + int32_t getValue(JSFunctionNativeCallContext& /*callContext*/) const { + return _value; + } + + void setValue(int32_t value, JSFunctionNativeCallContext& /*callContext*/) { + _value = value; + } + + int32_t getReadOnly(JSFunctionNativeCallContext& /*callContext*/) const { + return _value; + } + + void setWriteOnly(int32_t value, JSFunctionNativeCallContext& /*callContext*/) { + _value = value; + } + + int32_t requiredInt(int32_t value, JSFunctionNativeCallContext& /*callContext*/) { + _requiredCallCount++; + return value; + } + + int64_t echoLong(int64_t value, JSFunctionNativeCallContext& /*callContext*/) { + return value; + } + + int64_t largeLong(JSFunctionNativeCallContext& /*callContext*/) { + return kLargeLongValue; + } + + std::optional optionalInt(std::optional value, JSFunctionNativeCallContext& /*callContext*/) { + return value; + } + + std::optional optionalDouble(std::optional value, JSFunctionNativeCallContext& /*callContext*/) { + return value; + } + + std::optional optionalBool(std::optional value, JSFunctionNativeCallContext& /*callContext*/) { + return value; + } + + StringBox echoString(const StringBox& value, JSFunctionNativeCallContext& /*callContext*/) { + return value; + } + + Ref echoStaticString(const Ref& value, JSFunctionNativeCallContext& /*callContext*/) { + return value; + } + + Value echoValue(Value value, JSFunctionNativeCallContext& /*callContext*/) { + return value; + } + + int32_t typedArrayLength(const JSTypedArray& value, JSFunctionNativeCallContext& /*callContext*/) { + return static_cast(value.length); + } + + BytesView echoBytes(const BytesView& value, JSFunctionNativeCallContext& /*callContext*/) { + return value; + } + + Ref echoWrappedObject(const Ref& value, JSFunctionNativeCallContext& /*callContext*/) { + return value; + } + + JSValueRef echoJSValue(JSValue value, JSFunctionNativeCallContext& callContext) { + return JSValueRef::makeRetained(callContext.getContext(), value); + } + + static int32_t twice(int32_t value, JSFunctionNativeCallContext& /*callContext*/) { + return value * 2; + } + + static int32_t getSharedValue(JSFunctionNativeCallContext& /*callContext*/) { + return _sharedValue; + } + + static void setSharedValue(int32_t value, JSFunctionNativeCallContext& /*callContext*/) { + _sharedValue = value; + } + + static int32_t getClassReadOnly(JSFunctionNativeCallContext& /*callContext*/) { + return _sharedValue; + } + + static void setClassWriteOnly(int32_t value, JSFunctionNativeCallContext& /*callContext*/) { + _sharedValue = value; + } + + int32_t getRequiredCallCount() const { + return _requiredCallCount; + } + + IJavaScriptContext* getConstructorContext() const { + return _constructorContext; + } + + IJavaScriptContext* getLastContext() const { + return _lastContext; + } + + static void resetSharedValue() { + _sharedValue = 0; + } + +private: + int32_t _value; + int32_t _requiredCallCount = 0; + IJavaScriptContext* _constructorContext = nullptr; + IJavaScriptContext* _lastContext = nullptr; + static int32_t _sharedValue; +}; + +int32_t BinderTestObject::_sharedValue = 0; + +struct BinderTestValues { + Ref instanceOpaque; + JSValueRef cls; + JSValueRef instance; +}; + +static BinderTestValues setUpBinderTest(JSEntry& jsEntry) { + auto& context = jsEntry.context; + auto& exceptionTracker = jsEntry.exceptionTracker; + BinderTestObject::resetSharedValue(); + + auto longConstructor = context.evaluate("(function Long(low, high, unsigned) {" + " this.low = low;" + " this.high = high;" + " this.unsigned = unsigned;" + "})", + "JSNativeClassBinderLong.js", + exceptionTracker); + jsEntry.checkException(); + context.setLongConstructor(longConstructor); + + auto definition = + JSNativeClassBinder("BinderTestObject") + .bindConstructor() + .bindMethod<&BinderTestObject::add>("add", false, true, false) + .bindMethod<&BinderTestObject::reset>("reset") + .bindMethod<&BinderTestObject::requiredInt>("requiredInt") + .bindMethod<&BinderTestObject::echoLong>("echoLong") + .bindMethod<&BinderTestObject::largeLong>("largeLong") + .bindMethod<&BinderTestObject::optionalInt>("optionalInt") + .bindMethod<&BinderTestObject::optionalDouble>("optionalDouble") + .bindMethod<&BinderTestObject::optionalBool>("optionalBool") + .bindMethod<&BinderTestObject::echoString>("echoString") + .bindMethod<&BinderTestObject::echoStaticString>("echoStaticString") + .bindMethod<&BinderTestObject::echoValue>("echoValue") + .bindMethod<&BinderTestObject::typedArrayLength>("typedArrayLength") + .bindMethod<&BinderTestObject::echoBytes>("echoBytes") + .bindMethod<&BinderTestObject::echoWrappedObject>("echoWrappedObject") + .bindMethod<&BinderTestObject::echoJSValue>("echoJSValue") + .bindAccessor<&BinderTestObject::getValue, &BinderTestObject::setValue>("value") + .bindGetter<&BinderTestObject::getReadOnly>("readOnly") + .bindSetter<&BinderTestObject::setWriteOnly>("writeOnly") + .bindConstant("kind", context.newStringUTF8("binder-instance", exceptionTracker)) + .bindClassMethod<&BinderTestObject::twice>("twice") + .bindClassAccessor<&BinderTestObject::getSharedValue, &BinderTestObject::setSharedValue>("sharedValue") + .bindClassGetter<&BinderTestObject::getClassReadOnly>("classReadOnly") + .bindClassSetter<&BinderTestObject::setClassWriteOnly>("classWriteOnly") + .bindClassConstant("category", context.newStringUTF8("binder-class", exceptionTracker)) + .extractClassDefinition(); + jsEntry.checkException(); + + EXPECT_EQ(STRING_LITERAL("BinderTestObject"), definition.getName()); + for (const auto& entry : definition.getEntries()) { + EXPECT_FALSE(entry.getName().isNull()); + } + + auto cls = context.newNativeClass(nullptr, definition, exceptionTracker); + jsEntry.checkException(); + auto instanceOpaque = makeShared(10); + auto instance = context.newObjectFromNativeClass(instanceOpaque, cls.get(), exceptionTracker); + jsEntry.checkException(); + + auto global = context.getGlobalObject(exceptionTracker); + context.setObjectProperty(global.get(), "BinderTestObject", cls.get(), exceptionTracker); + context.setObjectProperty(global.get(), "binderTestObject", instance.get(), exceptionTracker); + jsEntry.checkException(); + + return BinderTestValues{ + std::move(instanceOpaque), + std::move(cls), + std::move(instance), + }; +} + +static JSValueRef evaluateBinderExpression(JSEntry& jsEntry, const char* source) { + auto value = jsEntry.context.evaluate(source, "JSNativeClassBinder_tests.js", jsEntry.exceptionTracker); + jsEntry.checkException(); + return value; +} + +TEST_P(JSNativeClassBinderTest, bindsConstructorAndInstanceMembers) { + MAIN_THREAD_INIT(); + auto wrapper = createWrapper(); + auto jsEntry = wrapper.makeJsEntry(); + auto& context = jsEntry.context; + auto& exceptionTracker = jsEntry.exceptionTracker; + auto values = setUpBinderTest(jsEntry); + + ASSERT_EQ(15, + context.valueToInt(evaluateBinderExpression(jsEntry, "binderTestObject.add(5)").get(), exceptionTracker)); + ASSERT_EQ(&context, values.instanceOpaque->getLastContext()); + + evaluateBinderExpression(jsEntry, "binderTestObject.value = 21"); + ASSERT_EQ(21, + context.valueToInt(evaluateBinderExpression(jsEntry, "binderTestObject.value").get(), exceptionTracker)); + ASSERT_EQ( + 21, context.valueToInt(evaluateBinderExpression(jsEntry, "binderTestObject.readOnly").get(), exceptionTracker)); + + evaluateBinderExpression(jsEntry, "binderTestObject.writeOnly = 8"); + ASSERT_EQ(8, + context.valueToInt(evaluateBinderExpression(jsEntry, "binderTestObject.value").get(), exceptionTracker)); + + auto resetResult = evaluateBinderExpression(jsEntry, "binderTestObject.reset()"); + ASSERT_TRUE(context.isValueUndefined(resetResult.get())); + + auto constructed = evaluateBinderExpression( + jsEntry, "globalThis.constructedBinderObject = new BinderTestObject(7); constructedBinderObject"); + auto constructedOpaque = + castOrNull(context.valueToWrappedObject(constructed.get(), exceptionTracker)); + ASSERT_NE(nullptr, constructedOpaque); + ASSERT_EQ( + 7, + context.valueToInt(evaluateBinderExpression(jsEntry, "constructedBinderObject.value").get(), exceptionTracker)); + ASSERT_EQ(7, + context.valueToInt(evaluateBinderExpression(jsEntry, "constructedBinderObject.constructedValue").get(), + exceptionTracker)); + ASSERT_EQ(&context, constructedOpaque->getConstructorContext()); + ASSERT_TRUE(context.valueToBool( + evaluateBinderExpression(jsEntry, "constructedBinderObject instanceof BinderTestObject").get(), + exceptionTracker)); + ASSERT_TRUE(context.valueToBool( + evaluateBinderExpression(jsEntry, "binderTestObject instanceof BinderTestObject").get(), exceptionTracker)); + ASSERT_EQ(STRING_LITERAL("function"), + context.valueToString(evaluateBinderExpression(jsEntry, "typeof BinderTestObject").get(), + exceptionTracker)); + ASSERT_EQ(STRING_LITERAL("BinderTestObject"), + context.valueToString(evaluateBinderExpression(jsEntry, "BinderTestObject.name").get(), + exceptionTracker)); + ASSERT_TRUE(context.valueToBool( + evaluateBinderExpression( + jsEntry, + "(() => {" + " try { BinderTestObject(1); }" + " catch (error) { return error.message === 'Native class constructor must be called with new'; }" + " return false;" + "})()") + .get(), + exceptionTracker)); + + ASSERT_EQ( + STRING_LITERAL("binder-instance"), + context.valueToString(evaluateBinderExpression(jsEntry, "binderTestObject.kind").get(), exceptionTracker)); + ASSERT_FALSE(context.valueToBool( + evaluateBinderExpression(jsEntry, "Object.getOwnPropertyDescriptor(BinderTestObject.prototype, 'add').writable") + .get(), + exceptionTracker)); + ASSERT_TRUE(context.valueToBool( + evaluateBinderExpression(jsEntry, + "Object.getOwnPropertyDescriptor(BinderTestObject.prototype, 'add').enumerable") + .get(), + exceptionTracker)); + ASSERT_FALSE(context.valueToBool( + evaluateBinderExpression(jsEntry, + "Object.getOwnPropertyDescriptor(BinderTestObject.prototype, 'add').configurable") + .get(), + exceptionTracker)); + jsEntry.checkException(); +} + +TEST_P(JSNativeClassBinderTest, bindsClassMembers) { + MAIN_THREAD_INIT(); + auto wrapper = createWrapper(); + auto jsEntry = wrapper.makeJsEntry(); + auto& context = jsEntry.context; + auto& exceptionTracker = jsEntry.exceptionTracker; + setUpBinderTest(jsEntry); + + ASSERT_EQ( + 12, context.valueToInt(evaluateBinderExpression(jsEntry, "BinderTestObject.twice(6)").get(), exceptionTracker)); + evaluateBinderExpression(jsEntry, "BinderTestObject.sharedValue = 9"); + ASSERT_EQ( + 9, + context.valueToInt(evaluateBinderExpression(jsEntry, "BinderTestObject.sharedValue").get(), exceptionTracker)); + ASSERT_EQ(9, + context.valueToInt(evaluateBinderExpression(jsEntry, "BinderTestObject.classReadOnly").get(), + exceptionTracker)); + + evaluateBinderExpression(jsEntry, "BinderTestObject.classWriteOnly = 14"); + ASSERT_EQ( + 14, + context.valueToInt(evaluateBinderExpression(jsEntry, "BinderTestObject.sharedValue").get(), exceptionTracker)); + ASSERT_EQ( + STRING_LITERAL("binder-class"), + context.valueToString(evaluateBinderExpression(jsEntry, "BinderTestObject.category").get(), exceptionTracker)); + jsEntry.checkException(); +} + +TEST_P(JSNativeClassBinderTest, convertsRequiredAndOptionalPrimitives) { + MAIN_THREAD_INIT(); + auto wrapper = createWrapper(); + auto jsEntry = wrapper.makeJsEntry(); + auto& context = jsEntry.context; + auto& exceptionTracker = jsEntry.exceptionTracker; + auto values = setUpBinderTest(jsEntry); + + ASSERT_EQ(4, + context.valueToInt(evaluateBinderExpression(jsEntry, "binderTestObject.requiredInt(4)").get(), + exceptionTracker)); + ASSERT_TRUE(context.valueToBool( + evaluateBinderExpression( + jsEntry, + "(() => { try { binderTestObject.requiredInt(null); return false; } catch (e) { return true; } })()") + .get(), + exceptionTracker)); + ASSERT_TRUE(context.valueToBool( + evaluateBinderExpression( + jsEntry, "(() => { try { binderTestObject.requiredInt(); return false; } catch (e) { return true; } })()") + .get(), + exceptionTracker)); + ASSERT_EQ(1, values.instanceOpaque->getRequiredCallCount()); + + auto longResult = evaluateBinderExpression(jsEntry, "binderTestObject.echoLong(binderTestObject.largeLong())"); + ASSERT_EQ(BinderTestObject::kLargeLongValue, context.valueToLong(longResult.get(), exceptionTracker).toInt64()); + ASSERT_TRUE(context.valueToBool( + evaluateBinderExpression( + jsEntry, "(() => { try { binderTestObject.echoLong(null); return false; } catch (e) { return true; } })()") + .get(), + exceptionTracker)); + + ASSERT_TRUE( + context.isValueUndefined(evaluateBinderExpression(jsEntry, "binderTestObject.optionalInt(null)").get())); + ASSERT_TRUE(context.isValueUndefined(evaluateBinderExpression(jsEntry, "binderTestObject.optionalInt()").get())); + ASSERT_EQ(5, + context.valueToInt(evaluateBinderExpression(jsEntry, "binderTestObject.optionalInt(5)").get(), + exceptionTracker)); + ASSERT_DOUBLE_EQ( + 1.5, + context.valueToDouble(evaluateBinderExpression(jsEntry, "binderTestObject.optionalDouble(1.5)").get(), + exceptionTracker)); + ASSERT_TRUE(context.valueToBool(evaluateBinderExpression(jsEntry, "binderTestObject.optionalBool(true)").get(), + exceptionTracker)); + jsEntry.checkException(); +} + +TEST_P(JSNativeClassBinderTest, convertsObjectLikeValues) { + MAIN_THREAD_INIT(); + auto wrapper = createWrapper(); + auto jsEntry = wrapper.makeJsEntry(); + auto& context = jsEntry.context; + auto& exceptionTracker = jsEntry.exceptionTracker; + auto values = setUpBinderTest(jsEntry); + + ASSERT_EQ(STRING_LITERAL("hello"), + context.valueToString(evaluateBinderExpression(jsEntry, "binderTestObject.echoString('hello')").get(), + exceptionTracker)); + ASSERT_TRUE(context.isValueUndefined(evaluateBinderExpression(jsEntry, "binderTestObject.echoString(null)").get())); + ASSERT_EQ( + STRING_LITERAL("static"), + context.valueToString(evaluateBinderExpression(jsEntry, "binderTestObject.echoStaticString('static')").get(), + exceptionTracker)); + ASSERT_EQ( + 3, + context.valueToInt(evaluateBinderExpression(jsEntry, "binderTestObject.echoValue({ count: 3 }).count").get(), + exceptionTracker)); + ASSERT_EQ( + 3, + context.valueToInt( + evaluateBinderExpression(jsEntry, "binderTestObject.typedArrayLength(new Uint8Array([1, 2, 3]))").get(), + exceptionTracker)); + ASSERT_EQ(2, + context.valueToInt( + evaluateBinderExpression(jsEntry, "binderTestObject.echoBytes(new Uint8Array([4, 5])).length").get(), + exceptionTracker)); + ASSERT_EQ(5, + context.valueToInt( + evaluateBinderExpression(jsEntry, "binderTestObject.echoBytes(new Uint8Array([4, 5]))[1]").get(), + exceptionTracker)); + + auto wrappedResult = evaluateBinderExpression(jsEntry, "binderTestObject.echoWrappedObject(binderTestObject)"); + ASSERT_EQ(values.instanceOpaque, + castOrNull(context.valueToWrappedObject(wrappedResult.get(), exceptionTracker))); + + ASSERT_TRUE( + context.valueToBool(evaluateBinderExpression(jsEntry, + "globalThis.rawBinderValue = { marker: 1 }; " + "binderTestObject.echoJSValue(rawBinderValue) === rawBinderValue") + .get(), + exceptionTracker)); + jsEntry.checkException(); +} + +INSTANTIATE_TEST_SUITE_P(JSNativeClassBinderTests, + JSNativeClassBinderTest, + ::testing::Values(JavaScriptEngineTestCase::QuickJS, + JavaScriptEngineTestCase::JSCore, + JavaScriptEngineTestCase::Hermes), + PrintJavaScriptEngineType()); + +} // namespace ValdiTest diff --git a/valdi/test/runtime/JavaScriptTypes_tests.cpp b/valdi/test/runtime/JavaScriptTypes_tests.cpp index 40cc56b4..651c4258 100644 --- a/valdi/test/runtime/JavaScriptTypes_tests.cpp +++ b/valdi/test/runtime/JavaScriptTypes_tests.cpp @@ -169,6 +169,18 @@ struct MockJavaScriptContext : public IJavaScriptContext { return JSValueRef(); } + JSValueRef newNativeClass(const Ref& classOpaque, + const JSClassDefinition& classDefinition, + JSExceptionTracker& exceptionTracker) override { + return JSValueRef(); + } + + JSValueRef newObjectFromNativeClass(const Ref& opaque, + const JSValue& cls, + JSExceptionTracker& exceptionTracker) override { + return JSValueRef(); + } + JSValueRef newWeakRef(const JSValue& value, JSExceptionTracker& exceptionTracker) override { return JSValueRef(); } diff --git a/valdi_core/src/valdi_core/cpp/Utils/DefaultValueMarshallers.hpp b/valdi_core/src/valdi_core/cpp/Utils/DefaultValueMarshallers.hpp index e8d14688..1b58b3c4 100644 --- a/valdi_core/src/valdi_core/cpp/Utils/DefaultValueMarshallers.hpp +++ b/valdi_core/src/valdi_core/cpp/Utils/DefaultValueMarshallers.hpp @@ -295,17 +295,7 @@ class StringValueMarshaller : public ValueMarshaller { if (value.isInternedString()) { return this->_delegate->newStringUTF8(value.toStringBox().toStringView(), exceptionTracker); } else { - const auto* staticString = value.getStaticString(); - switch (staticString->encoding()) { - case StaticString::Encoding::UTF8: - return this->_delegate->newStringUTF8(staticString->utf8StringView(), exceptionTracker); - case StaticString::Encoding::UTF16: - return this->_delegate->newStringUTF16(staticString->utf16StringView(), exceptionTracker); - case StaticString::Encoding::UTF32: { - auto storage = staticString->utf8Storage(); - return this->_delegate->newStringUTF8(storage.toStringView(), exceptionTracker); - } - } + return this->_delegate->newString(*value.getStaticString(), exceptionTracker); } } diff --git a/valdi_core/src/valdi_core/cpp/Utils/PlatformValueDelegate.hpp b/valdi_core/src/valdi_core/cpp/Utils/PlatformValueDelegate.hpp index 145e1fc1..733b32da 100644 --- a/valdi_core/src/valdi_core/cpp/Utils/PlatformValueDelegate.hpp +++ b/valdi_core/src/valdi_core/cpp/Utils/PlatformValueDelegate.hpp @@ -12,6 +12,7 @@ #include "valdi_core/cpp/Utils/ExceptionTracker.hpp" #include "valdi_core/cpp/Utils/Function.hpp" #include "valdi_core/cpp/Utils/Result.hpp" +#include "valdi_core/cpp/Utils/StaticString.hpp" #include "valdi_core/cpp/Utils/Value.hpp" #include "valdi_core/cpp/Utils/ValueFunction.hpp" #include "valdi_core/cpp/Utils/ValueTypedArray.hpp" @@ -279,6 +280,19 @@ class PlatformValueDelegate : public SimpleRefCountable { virtual ValueType newStringUTF16(std::u16string_view str, ExceptionTracker& exceptionTracker) = 0; + virtual ValueType newString(const StaticString& str, ExceptionTracker& exceptionTracker) { + switch (str.encoding()) { + case StaticString::Encoding::UTF8: + return newStringUTF8(str.utf8StringView(), exceptionTracker); + case StaticString::Encoding::UTF16: + return newStringUTF16(str.utf16StringView(), exceptionTracker); + case StaticString::Encoding::UTF32: { + auto utf8 = str.utf8Storage(); + return newStringUTF8(utf8.toStringView(), exceptionTracker); + } + } + } + virtual ValueType newByteArray(const BytesView& bytes, ExceptionTracker& exceptionTracker) = 0; virtual ValueType newTypedArray(TypedArrayType arrayType,