xbintsc 0.5.32 → 0.5.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/README.md +10 -7
  2. package/README.zh-CN.md +10 -8
  3. package/dist/scripts/check-version.d.ts +27 -0
  4. package/dist/scripts/check-version.js +71 -0
  5. package/dist/scripts/check-version.js.map +1 -0
  6. package/dist/scripts/package-platform.d.ts +58 -0
  7. package/dist/scripts/package-platform.js +142 -0
  8. package/dist/scripts/package-platform.js.map +1 -0
  9. package/dist/scripts/verify-platform-package.d.ts +34 -0
  10. package/dist/scripts/verify-platform-package.js +161 -0
  11. package/dist/scripts/verify-platform-package.js.map +1 -0
  12. package/dist/src/cli/help.d.ts +1 -1
  13. package/dist/src/cli/main.js +3 -1
  14. package/dist/src/cli/main.js.map +1 -1
  15. package/dist/src/driver/runtime-lib.d.ts +29 -4
  16. package/dist/src/driver/runtime-lib.js +68 -10
  17. package/dist/src/driver/runtime-lib.js.map +1 -1
  18. package/dist/src/driver/version.d.ts +1 -1
  19. package/dist/src/driver/version.js +1 -1
  20. package/dist/src/extensions/gui/index.js +18 -5
  21. package/dist/src/extensions/gui/index.js.map +1 -1
  22. package/dist/src/extensions/media/index.js +4 -3
  23. package/dist/src/extensions/media/index.js.map +1 -1
  24. package/dist/src/parser/context.d.ts +41 -0
  25. package/dist/src/parser/context.js +89 -4
  26. package/dist/src/parser/context.js.map +1 -1
  27. package/dist/src/parser/literals.js +6 -12
  28. package/dist/src/parser/literals.js.map +1 -1
  29. package/dist/tests/driver/platform-package.test.d.ts +1 -0
  30. package/dist/tests/driver/platform-package.test.js +49 -0
  31. package/dist/tests/driver/platform-package.test.js.map +1 -0
  32. package/dist/tests/driver/runtime-lib.test.js +51 -4
  33. package/dist/tests/driver/runtime-lib.test.js.map +1 -1
  34. package/dist/tests/e2e/gui-audio.test.d.ts +14 -0
  35. package/dist/tests/e2e/gui-audio.test.js +287 -0
  36. package/dist/tests/e2e/gui-audio.test.js.map +1 -0
  37. package/dist/tests/e2e/gui-browser.test.js +65 -0
  38. package/dist/tests/e2e/gui-browser.test.js.map +1 -1
  39. package/dist/tests/e2e/gui-canvas-args.test.d.ts +6 -0
  40. package/dist/tests/e2e/gui-canvas-args.test.js +217 -0
  41. package/dist/tests/e2e/gui-canvas-args.test.js.map +1 -0
  42. package/dist/tests/e2e/gui-canvas.test.js.map +1 -1
  43. package/dist/tests/e2e/gui-dom.test.js +215 -0
  44. package/dist/tests/e2e/gui-dom.test.js.map +1 -1
  45. package/dist/tests/e2e/gui-events.test.js +112 -0
  46. package/dist/tests/e2e/gui-events.test.js.map +1 -1
  47. package/dist/tests/e2e/gui-helpers.d.ts +7 -0
  48. package/dist/tests/e2e/gui-helpers.js +1 -0
  49. package/dist/tests/e2e/gui-helpers.js.map +1 -1
  50. package/dist/tests/e2e/gui-media.test.js +78 -0
  51. package/dist/tests/e2e/gui-media.test.js.map +1 -1
  52. package/dist/tests/e2e/gui-style.test.js +55 -0
  53. package/dist/tests/e2e/gui-style.test.js.map +1 -1
  54. package/dist/tests/e2e/gui-webgl.test.js +258 -0
  55. package/dist/tests/e2e/gui-webgl.test.js.map +1 -1
  56. package/dist/tests/e2e/gui-webgl2-3d.test.d.ts +4 -0
  57. package/dist/tests/e2e/gui-webgl2-3d.test.js +264 -0
  58. package/dist/tests/e2e/gui-webgl2-3d.test.js.map +1 -0
  59. package/dist/tests/e2e/gui-webgl2-blit.test.d.ts +10 -0
  60. package/dist/tests/e2e/gui-webgl2-blit.test.js +262 -0
  61. package/dist/tests/e2e/gui-webgl2-blit.test.js.map +1 -0
  62. package/dist/tests/e2e/gui-webgl2-formats.test.d.ts +6 -0
  63. package/dist/tests/e2e/gui-webgl2-formats.test.js +395 -0
  64. package/dist/tests/e2e/gui-webgl2-formats.test.js.map +1 -0
  65. package/dist/tests/e2e/gui-webgl2-mrt.test.d.ts +4 -0
  66. package/dist/tests/e2e/gui-webgl2-mrt.test.js +105 -0
  67. package/dist/tests/e2e/gui-webgl2-mrt.test.js.map +1 -0
  68. package/dist/tests/e2e/gui-webgl2-ubo.test.d.ts +8 -0
  69. package/dist/tests/e2e/gui-webgl2-ubo.test.js +112 -0
  70. package/dist/tests/e2e/gui-webgl2-ubo.test.js.map +1 -0
  71. package/dist/tests/e2e/gui-webgl2.test.js +45 -1
  72. package/dist/tests/e2e/gui-webgl2.test.js.map +1 -1
  73. package/dist/tests/extensions/gui-index.test.js +1 -0
  74. package/dist/tests/extensions/gui-index.test.js.map +1 -1
  75. package/dist/tests/extensions/media.test.js +1 -0
  76. package/dist/tests/extensions/media.test.js.map +1 -1
  77. package/dist/tests/parser/expressions.test.js +30 -0
  78. package/dist/tests/parser/expressions.test.js.map +1 -1
  79. package/dist/tests/platform-tarball.test.d.ts +1 -0
  80. package/dist/tests/platform-tarball.test.js +111 -0
  81. package/dist/tests/platform-tarball.test.js.map +1 -0
  82. package/dist/tests/version.test.js +6 -0
  83. package/dist/tests/version.test.js.map +1 -1
  84. package/doc/ai/contributing.md +11 -0
  85. package/doc/ai/extensions.md +7 -4
  86. package/doc/ai/troubleshooting.md +7 -5
  87. package/doc/ai/zh-CN/contributing.md +9 -0
  88. package/doc/ai/zh-CN/extensions.md +4 -2
  89. package/doc/ai/zh-CN/troubleshooting.md +5 -3
  90. package/doc/gui/api.md +99 -22
  91. package/doc/gui/build.md +11 -4
  92. package/doc/gui/canvas.md +49 -18
  93. package/doc/gui/dom.md +57 -7
  94. package/doc/gui/media.md +4 -0
  95. package/doc/gui/webgl.md +105 -46
  96. package/doc/gui.md +7 -3
  97. package/doc/npm-package.md +55 -27
  98. package/doc/requirements.md +6 -2
  99. package/doc/zh-CN/gui/api.md +84 -14
  100. package/doc/zh-CN/gui/build.md +7 -2
  101. package/doc/zh-CN/gui/canvas.md +44 -19
  102. package/doc/zh-CN/gui/dom.md +48 -4
  103. package/doc/zh-CN/gui/media.md +3 -0
  104. package/doc/zh-CN/gui/webgl.md +22 -16
  105. package/doc/zh-CN/gui.md +3 -2
  106. package/doc/zh-CN/npm-package.md +44 -23
  107. package/doc/zh-CN/requirements.md +4 -1
  108. package/package.json +17 -1
  109. package/runtime/ext_gui/audio.cpp +365 -153
  110. package/runtime/ext_gui/audio_buffer.cpp +166 -0
  111. package/runtime/ext_gui/audio_buffer.h +37 -0
  112. package/runtime/ext_gui/audio_decode.cpp +219 -0
  113. package/runtime/ext_gui/audio_decode.h +39 -0
  114. package/runtime/ext_gui/audio_device.cpp +87 -0
  115. package/runtime/ext_gui/audio_effects.cpp +163 -0
  116. package/runtime/ext_gui/audio_effects.h +25 -0
  117. package/runtime/ext_gui/audio_internal.h +30 -0
  118. package/runtime/ext_gui/audio_mixer.cpp +494 -0
  119. package/runtime/ext_gui/audio_mixer.h +238 -0
  120. package/runtime/ext_gui/audio_param.cpp +104 -0
  121. package/runtime/ext_gui/audio_param_object.cpp +155 -0
  122. package/runtime/ext_gui/browser.cpp +45 -50
  123. package/runtime/ext_gui/browser_abort.cpp +249 -0
  124. package/runtime/ext_gui/browser_dialogs.cpp +76 -0
  125. package/runtime/ext_gui/browser_media.cpp +176 -0
  126. package/runtime/ext_gui/browser_navigation.cpp +130 -0
  127. package/runtime/ext_gui/canvas.cpp +88 -30
  128. package/runtime/ext_gui/canvas.h +74 -9
  129. package/runtime/ext_gui/canvas_image_data.cpp +143 -0
  130. package/runtime/ext_gui/canvas_internal.h +8 -5
  131. package/runtime/ext_gui/canvas_pattern.cpp +90 -0
  132. package/runtime/ext_gui/canvas_raster.cpp +31 -9
  133. package/runtime/ext_gui/canvas_style.cpp +4 -2
  134. package/runtime/ext_gui/css_selector.cpp +21 -3
  135. package/runtime/ext_gui/dom.h +4 -0
  136. package/runtime/ext_gui/dom_api.cpp +1 -0
  137. package/runtime/ext_gui/dom_api_canvas.cpp +120 -114
  138. package/runtime/ext_gui/dom_api_canvas_extra.cpp +57 -2
  139. package/runtime/ext_gui/dom_api_canvas_image.cpp +239 -0
  140. package/runtime/ext_gui/dom_api_canvas_paths.cpp +179 -0
  141. package/runtime/ext_gui/dom_api_computed_style.cpp +124 -0
  142. package/runtime/ext_gui/dom_api_dataset.cpp +154 -0
  143. package/runtime/ext_gui/dom_api_document.cpp +58 -5
  144. package/runtime/ext_gui/dom_api_events.cpp +93 -17
  145. package/runtime/ext_gui/dom_api_internal.h +71 -2
  146. package/runtime/ext_gui/dom_api_mutation.cpp +159 -0
  147. package/runtime/ext_gui/dom_api_node.cpp +40 -19
  148. package/runtime/ext_gui/dom_api_node_read.cpp +37 -1
  149. package/runtime/ext_gui/dom_api_path2d.cpp +358 -0
  150. package/runtime/ext_gui/dom_api_proto.cpp +35 -0
  151. package/runtime/ext_gui/dom_api_style.cpp +223 -13
  152. package/runtime/ext_gui/dom_api_webgl.cpp +7 -0
  153. package/runtime/ext_gui/dom_api_webgl2.cpp +151 -36
  154. package/runtime/ext_gui/dom_api_webgl2_draw.cpp +3 -3
  155. package/runtime/ext_gui/dom_api_webgl_buffer.cpp +6 -2
  156. package/runtime/ext_gui/dom_api_webgl_compressed.cpp +167 -0
  157. package/runtime/ext_gui/dom_api_webgl_framebuffer.cpp +160 -0
  158. package/runtime/ext_gui/dom_api_webgl_shader.cpp +4 -4
  159. package/runtime/ext_gui/dom_api_webgl_source.cpp +161 -0
  160. package/runtime/ext_gui/dom_api_webgl_state.cpp +169 -23
  161. package/runtime/ext_gui/dom_api_webgl_texture.cpp +276 -236
  162. package/runtime/ext_gui/dom_api_webgl_ubo.cpp +81 -0
  163. package/runtime/ext_gui/gui.cpp +103 -14
  164. package/runtime/ext_gui/gui_engine.h +56 -4
  165. package/runtime/ext_gui/gui_events.cpp +80 -9
  166. package/runtime/ext_gui/image_bitmap.cpp +28 -0
  167. package/runtime/ext_gui/image_bitmap.h +26 -0
  168. package/runtime/ext_gui/image_bitmap_browser.cpp +136 -0
  169. package/runtime/ext_gui/listener_options.h +43 -0
  170. package/runtime/ext_gui/media.cpp +32 -0
  171. package/runtime/ext_gui/media.h +13 -0
  172. package/runtime/ext_gui/media_api.h +23 -0
  173. package/runtime/ext_gui/paint.cpp +28 -0
  174. package/runtime/ext_gui/paint.h +4 -0
  175. package/runtime/ext_gui/webgl.h +152 -18
  176. package/runtime/ext_gui/webgl_blit.cpp +95 -0
  177. package/runtime/ext_gui/webgl_buffer.cpp +178 -263
  178. package/runtime/ext_gui/webgl_buffer_objects.cpp +270 -0
  179. package/runtime/ext_gui/webgl_clear.cpp +142 -0
  180. package/runtime/ext_gui/webgl_compressed.cpp +180 -0
  181. package/runtime/ext_gui/webgl_context.cpp +241 -156
  182. package/runtime/ext_gui/webgl_draw.cpp +68 -3
  183. package/runtime/ext_gui/webgl_draw_buffers.cpp +75 -0
  184. package/runtime/ext_gui/webgl_glsl.h +40 -0
  185. package/runtime/ext_gui/webgl_glsl_compile.cpp +6 -0
  186. package/runtime/ext_gui/webgl_glsl_rewrite.cpp +18 -0
  187. package/runtime/ext_gui/webgl_glsl_scan.cpp +20 -11
  188. package/runtime/ext_gui/webgl_glsl_scan_block.cpp +126 -0
  189. package/runtime/ext_gui/webgl_glsl_tokens.h +28 -0
  190. package/runtime/ext_gui/webgl_pipeline.cpp +71 -19
  191. package/runtime/ext_gui/webgl_program_blocks.cpp +58 -0
  192. package/runtime/ext_gui/webgl_program_internal.h +6 -0
  193. package/runtime/ext_gui/webgl_shader.cpp +13 -1
  194. package/runtime/ext_gui/webgl_state.cpp +228 -0
  195. package/runtime/ext_gui/webgl_texture.cpp +144 -17
  196. package/runtime/ext_gui/webgl_texture_3d.cpp +215 -0
  197. package/runtime/ext_gui/webgl_uniform_block.h +31 -0
  198. package/runtime/ext_gui/webgl_webgl2.cpp +58 -5
  199. package/runtime/ext_gui/window.cpp +14 -65
  200. package/runtime/ext_gui/window_events.cpp +125 -0
  201. package/runtime/ext_media/decode_audio.cpp +308 -0
  202. package/runtime/ext_media/media_bridge.c +2 -0
  203. package/runtime/rt.h +43 -0
  204. package/runtime/rt_internal.h +4 -0
  205. package/runtime/xt_array_buffer.c +30 -0
  206. package/runtime/xt_builtins.c +0 -21
  207. package/runtime/xt_containers/access.inc +94 -2
  208. package/runtime/xt_containers/objects.inc +29 -31
  209. package/runtime/xt_typed_array/elements.inc +13 -0
  210. package/scripts/build-media.ts +14 -6
  211. package/scripts/build-runtime.ts +21 -16
  212. package/scripts/check-version.ts +39 -9
  213. package/scripts/package-platform.ts +183 -0
  214. package/scripts/set-platform-version.ts +62 -0
  215. package/scripts/verify-platform-package.ts +182 -0
  216. package/src/cli/main.ts +5 -1
  217. package/src/driver/runtime-lib.ts +73 -9
  218. package/src/driver/version.ts +1 -1
  219. package/src/extensions/gui/index.ts +18 -5
  220. package/src/extensions/media/index.ts +4 -3
  221. package/src/parser/context.ts +99 -4
  222. package/src/parser/literals.ts +6 -12
@@ -0,0 +1,308 @@
1
+ /*
2
+ * xbintsc media extension — in-memory audio decoding.
3
+ *
4
+ * `AudioContext.decodeAudioData` hands over bytes, not a path, so the file
5
+ * oriented `MediaPlayer` cannot be reused. This unit opens the bytes through a
6
+ * custom `AVIOContext`, decodes the best audio stream to interleaved float PCM
7
+ * at the stream's own sample rate, and leaves resampling to the mixer (which
8
+ * already resamples an AudioBuffer whose rate differs from 48 kHz).
9
+ *
10
+ * The ABI is C (`media_api.h`): `gui.a` stays FFmpeg-free and reaches this
11
+ * object through the table the media bridge installs. Both archives are built
12
+ * against the same static CRT, so the caller may `free()` the buffer through
13
+ * `xt_media_backend_free_audio`.
14
+ */
15
+
16
+ #include <cerrno>
17
+ #include <cstdio>
18
+ #include <cstdlib>
19
+ #include <cstring>
20
+ #include <vector>
21
+
22
+ extern "C" {
23
+ #include <libavcodec/avcodec.h>
24
+ #include <libavformat/avformat.h>
25
+ #include <libavutil/channel_layout.h>
26
+ #include <libswresample/swresample.h>
27
+ }
28
+
29
+ #include "../ext_gui/media_api.h"
30
+
31
+ namespace {
32
+
33
+ struct MemoryReader {
34
+ const unsigned char *data;
35
+ size_t size;
36
+ size_t position;
37
+ };
38
+
39
+ int read_packet(void *opaque, uint8_t *buffer, int size) {
40
+ auto *reader = static_cast<MemoryReader *>(opaque);
41
+ if (reader->position >= reader->size) return AVERROR_EOF;
42
+ size_t available = reader->size - reader->position;
43
+ int take = size < static_cast<int>(available) ? size : static_cast<int>(available);
44
+ if (take <= 0) return AVERROR_EOF;
45
+ std::memcpy(buffer, reader->data + reader->position, static_cast<size_t>(take));
46
+ reader->position += static_cast<size_t>(take);
47
+ return take;
48
+ }
49
+
50
+ int64_t seek_stream(void *opaque, int64_t offset, int whence) {
51
+ auto *reader = static_cast<MemoryReader *>(opaque);
52
+ if (whence == AVSEEK_SIZE) return static_cast<int64_t>(reader->size);
53
+ int64_t base;
54
+ if (whence == SEEK_SET) {
55
+ base = 0;
56
+ } else if (whence == SEEK_CUR) {
57
+ base = static_cast<int64_t>(reader->position);
58
+ } else if (whence == SEEK_END) {
59
+ base = static_cast<int64_t>(reader->size);
60
+ } else {
61
+ return AVERROR(EINVAL);
62
+ }
63
+ int64_t target = base + offset;
64
+ if (target < 0 || target > static_cast<int64_t>(reader->size)) return AVERROR(EINVAL);
65
+ reader->position = static_cast<size_t>(target);
66
+ return target;
67
+ }
68
+
69
+ void set_error(char *error, size_t size, const char *message) {
70
+ if (error != nullptr && size > 0) std::snprintf(error, size, "%s", message);
71
+ }
72
+
73
+ void set_ffmpeg_error(char *error, size_t size, const char *prefix, int code) {
74
+ char detail[AV_ERROR_MAX_STRING_SIZE] = {0};
75
+ av_strerror(code, detail, sizeof(detail));
76
+ if (error != nullptr && size > 0) {
77
+ std::snprintf(error, size, "%s: %s", prefix, detail);
78
+ }
79
+ }
80
+
81
+ /** The decoder pipeline for one in-memory file. Destructor releases everything,
82
+ * which keeps the early-return paths in `decode` readable. */
83
+ struct Decoder {
84
+ MemoryReader reader{nullptr, 0, 0};
85
+ AVIOContext *io = nullptr;
86
+ AVFormatContext *format = nullptr;
87
+ AVCodecContext *codec = nullptr;
88
+ AVPacket *packet = nullptr;
89
+ AVFrame *frame = nullptr;
90
+ SwrContext *resampler = nullptr;
91
+ AVChannelLayout out_layout{};
92
+ int out_channels = 0;
93
+ int sample_rate = 0;
94
+ int stream_index = -1;
95
+ std::vector<float> samples;
96
+ bool failed = false;
97
+ };
98
+
99
+ void close_decoder(Decoder *decoder) {
100
+ swr_free(&decoder->resampler);
101
+ av_channel_layout_uninit(&decoder->out_layout);
102
+ if (decoder->frame != nullptr) av_frame_free(&decoder->frame);
103
+ if (decoder->packet != nullptr) av_packet_free(&decoder->packet);
104
+ if (decoder->codec != nullptr) avcodec_free_context(&decoder->codec);
105
+ if (decoder->format != nullptr) avformat_close_input(&decoder->format);
106
+ if (decoder->io != nullptr) {
107
+ av_freep(&decoder->io->buffer);
108
+ avio_context_free(&decoder->io);
109
+ }
110
+ }
111
+
112
+ /** Lazily configure the resampler from the first decoded frame: some files only
113
+ * reveal their channel layout once decoding starts. */
114
+ bool prepare_resampler(Decoder *decoder, const AVFrame *frame, char *error, size_t error_size) {
115
+ if (decoder->resampler != nullptr) return true;
116
+ AVChannelLayout in_layout = frame->ch_layout;
117
+ if (in_layout.nb_channels <= 0) in_layout = decoder->codec->ch_layout;
118
+ int channels = in_layout.nb_channels > 0 ? in_layout.nb_channels : 1;
119
+ int rate = frame->sample_rate > 0 ? frame->sample_rate : decoder->codec->sample_rate;
120
+ if (rate <= 0) {
121
+ set_error(error, error_size, "audio stream has no usable sample rate");
122
+ return false;
123
+ }
124
+ decoder->out_channels = channels > 2 ? 2 : channels;
125
+ decoder->sample_rate = rate;
126
+ av_channel_layout_default(&decoder->out_layout, decoder->out_channels);
127
+ int result = swr_alloc_set_opts2(&decoder->resampler, &decoder->out_layout, AV_SAMPLE_FMT_FLT,
128
+ decoder->sample_rate, &in_layout,
129
+ static_cast<AVSampleFormat>(frame->format), rate, 0, nullptr);
130
+ if (result < 0 || decoder->resampler == nullptr ||
131
+ swr_init(decoder->resampler) < 0) {
132
+ set_error(error, error_size, "cannot set up audio resampling");
133
+ return false;
134
+ }
135
+ return true;
136
+ }
137
+
138
+ /** Convert one decoded frame and append it to `decoder->samples`. */
139
+ bool append_frame(Decoder *decoder, const AVFrame *frame, char *error, size_t error_size) {
140
+ if (!prepare_resampler(decoder, frame, error, error_size)) return false;
141
+ int64_t delay = swr_get_delay(decoder->resampler, frame->sample_rate);
142
+ int capacity = static_cast<int>(av_rescale_rnd(delay + frame->nb_samples, decoder->sample_rate,
143
+ frame->sample_rate, AV_ROUND_UP));
144
+ if (capacity <= 0) return true;
145
+ std::vector<float> converted(static_cast<size_t>(capacity) * decoder->out_channels);
146
+ uint8_t *out_data[1] = {reinterpret_cast<uint8_t *>(converted.data())};
147
+ int got = swr_convert(decoder->resampler, out_data, capacity,
148
+ const_cast<const uint8_t **>(frame->extended_data), frame->nb_samples);
149
+ if (got < 0) {
150
+ set_ffmpeg_error(error, error_size, "cannot resample audio", got);
151
+ return false;
152
+ }
153
+ size_t count = static_cast<size_t>(got) * decoder->out_channels;
154
+ decoder->samples.insert(decoder->samples.end(), converted.begin(),
155
+ converted.begin() + static_cast<std::ptrdiff_t>(count));
156
+ return true;
157
+ }
158
+
159
+ /** Drain the decoder after the last packet. */
160
+ bool flush_decoder(Decoder *decoder, char *error, size_t error_size) {
161
+ int result = avcodec_send_packet(decoder->codec, nullptr);
162
+ if (result < 0 && result != AVERROR_EOF) {
163
+ set_ffmpeg_error(error, error_size, "cannot flush audio decoder", result);
164
+ return false;
165
+ }
166
+ while (true) {
167
+ result = avcodec_receive_frame(decoder->codec, decoder->frame);
168
+ if (result == AVERROR(EAGAIN) || result == AVERROR_EOF) return true;
169
+ if (result < 0) {
170
+ set_ffmpeg_error(error, error_size, "cannot read audio frame", result);
171
+ return false;
172
+ }
173
+ if (!append_frame(decoder, decoder->frame, error, error_size)) return false;
174
+ av_frame_unref(decoder->frame);
175
+ }
176
+ }
177
+
178
+ bool decode_packets(Decoder *decoder, char *error, size_t error_size) {
179
+ while (true) {
180
+ int result = av_read_frame(decoder->format, decoder->packet);
181
+ if (result == AVERROR_EOF) break;
182
+ if (result < 0) {
183
+ set_ffmpeg_error(error, error_size, "cannot read audio packet", result);
184
+ return false;
185
+ }
186
+ if (decoder->packet->stream_index != decoder->stream_index) {
187
+ av_packet_unref(decoder->packet);
188
+ continue;
189
+ }
190
+ int sent = avcodec_send_packet(decoder->codec, decoder->packet);
191
+ av_packet_unref(decoder->packet);
192
+ if (sent < 0 && sent != AVERROR(EAGAIN)) {
193
+ set_ffmpeg_error(error, error_size, "cannot decode audio packet", sent);
194
+ return false;
195
+ }
196
+ while (true) {
197
+ result = avcodec_receive_frame(decoder->codec, decoder->frame);
198
+ if (result == AVERROR(EAGAIN) || result == AVERROR_EOF) break;
199
+ if (result < 0) {
200
+ set_ffmpeg_error(error, error_size, "cannot decode audio frame", result);
201
+ return false;
202
+ }
203
+ if (!append_frame(decoder, decoder->frame, error, error_size)) return false;
204
+ av_frame_unref(decoder->frame);
205
+ }
206
+ }
207
+ return flush_decoder(decoder, error, error_size);
208
+ }
209
+
210
+ bool open_decoder(Decoder *decoder, int *stream_index, char *error, size_t error_size) {
211
+ decoder->io = avio_alloc_context(
212
+ static_cast<unsigned char *>(av_malloc(32768)), 32768, 0, &decoder->reader, read_packet,
213
+ nullptr, seek_stream);
214
+ if (decoder->io == nullptr) {
215
+ set_error(error, error_size, "cannot allocate the audio reader");
216
+ return false;
217
+ }
218
+ decoder->format = avformat_alloc_context();
219
+ if (decoder->format == nullptr) {
220
+ set_error(error, error_size, "cannot allocate the audio container");
221
+ return false;
222
+ }
223
+ decoder->format->pb = decoder->io;
224
+ decoder->format->flags |= AVFMT_FLAG_CUSTOM_IO;
225
+ int result = avformat_open_input(&decoder->format, nullptr, nullptr, nullptr);
226
+ if (result < 0) {
227
+ set_ffmpeg_error(error, error_size, "cannot open audio data", result);
228
+ return false;
229
+ }
230
+ result = avformat_find_stream_info(decoder->format, nullptr);
231
+ if (result < 0) {
232
+ set_ffmpeg_error(error, error_size, "cannot read audio metadata", result);
233
+ return false;
234
+ }
235
+ *stream_index = av_find_best_stream(decoder->format, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);
236
+ if (*stream_index < 0) {
237
+ set_error(error, error_size, "audio data has no audio stream");
238
+ return false;
239
+ }
240
+ decoder->stream_index = *stream_index;
241
+ AVCodecParameters *parameters = decoder->format->streams[*stream_index]->codecpar;
242
+ const AVCodec *codec = avcodec_find_decoder(parameters->codec_id);
243
+ if (codec == nullptr) {
244
+ set_error(error, error_size, "no decoder for this audio codec");
245
+ return false;
246
+ }
247
+ decoder->codec = avcodec_alloc_context3(codec);
248
+ if (decoder->codec == nullptr) {
249
+ set_error(error, error_size, "cannot allocate the audio decoder");
250
+ return false;
251
+ }
252
+ result = avcodec_parameters_to_context(decoder->codec, parameters);
253
+ if (result < 0) {
254
+ set_ffmpeg_error(error, error_size, "cannot configure the audio decoder", result);
255
+ return false;
256
+ }
257
+ result = avcodec_open2(decoder->codec, codec, nullptr);
258
+ if (result < 0) {
259
+ set_ffmpeg_error(error, error_size, "cannot start the audio decoder", result);
260
+ return false;
261
+ }
262
+ decoder->packet = av_packet_alloc();
263
+ decoder->frame = av_frame_alloc();
264
+ if (decoder->packet == nullptr || decoder->frame == nullptr) {
265
+ set_error(error, error_size, "cannot allocate decoder buffers");
266
+ return false;
267
+ }
268
+ return true;
269
+ }
270
+
271
+ } // namespace
272
+
273
+ extern "C" int xt_media_backend_decode_audio(const unsigned char *data, size_t size,
274
+ XtMediaAudioBuffer *out, char *error,
275
+ size_t error_size) {
276
+ if (data == nullptr || size == 0 || out == nullptr) {
277
+ set_error(error, error_size, "empty audio data");
278
+ return 0;
279
+ }
280
+ std::memset(out, 0, sizeof(*out));
281
+ Decoder decoder;
282
+ decoder.reader = {data, size, 0};
283
+ int stream_index = -1;
284
+ bool ok = open_decoder(&decoder, &stream_index, error, error_size) &&
285
+ decode_packets(&decoder, error, error_size);
286
+ if (!ok || decoder.out_channels <= 0 || decoder.samples.empty()) {
287
+ if (ok) set_error(error, error_size, "audio data has no samples");
288
+ close_decoder(&decoder);
289
+ return 0;
290
+ }
291
+ int frames = static_cast<int>(decoder.samples.size() / decoder.out_channels);
292
+ size_t bytes = decoder.samples.size() * sizeof(float);
293
+ auto *samples = static_cast<float *>(std::malloc(bytes));
294
+ if (samples == nullptr) {
295
+ set_error(error, error_size, "cannot allocate the decoded audio buffer");
296
+ close_decoder(&decoder);
297
+ return 0;
298
+ }
299
+ std::memcpy(samples, decoder.samples.data(), bytes);
300
+ out->channels = decoder.out_channels;
301
+ out->sample_rate = decoder.sample_rate;
302
+ out->frames = frames;
303
+ out->samples = samples;
304
+ close_decoder(&decoder);
305
+ return 1;
306
+ }
307
+
308
+ extern "C" void xt_media_backend_free_audio(void *samples) { std::free(samples); }
@@ -22,6 +22,8 @@ static const XtMediaApi kMediaApi = {
22
22
  xt_media_backend_open,
23
23
  xt_media_backend_by_id,
24
24
  xt_media_backend_shutdown,
25
+ xt_media_backend_decode_audio,
26
+ xt_media_backend_free_audio,
25
27
  };
26
28
 
27
29
  #if defined(__GNUC__) || defined(__clang__)
package/runtime/rt.h CHANGED
@@ -185,6 +185,32 @@ xt_value xt_not(xt_value a);
185
185
  /* -- objects -------------------------------------------------------------- */
186
186
  xt_value xt_object_new(void);
187
187
  xt_value xt_object_new_with_proto(xt_value proto);
188
+
189
+ /*
190
+ * Optional dynamic-property hooks.
191
+ *
192
+ * A host object that represents a *view* over external state — the GUI engine's
193
+ * `element.style`, `element.dataset` and `getComputedStyle()` — cannot know its
194
+ * property names when the prototype is built. Such an object installs these
195
+ * hooks; they are consulted only when ordinary property lookup/assignment finds
196
+ * nothing, so real properties and accessors keep precedence.
197
+ *
198
+ * `get` returns `XT_UNDEFINED` when it does not handle the key. `set`, `has`
199
+ * and `remove` return non-zero when they handled the operation; `keys` returns
200
+ * an array of the dynamically visible string keys (or `XT_UNDEFINED`).
201
+ * `data` is the host pointer handed to `xt_object_set_hooks`. Hooks must not
202
+ * store `xt_value`s outside a runtime object the collector can reach.
203
+ */
204
+ typedef struct xt_object_hooks {
205
+ xt_value (*get)(xt_value self, xt_value key, void *data);
206
+ int (*set)(xt_value self, xt_value key, xt_value value, void *data);
207
+ int (*has)(xt_value self, xt_value key, void *data);
208
+ int (*remove)(xt_value self, xt_value key, void *data);
209
+ xt_value (*keys)(xt_value self, void *data);
210
+ } xt_object_hooks;
211
+
212
+ /** Install (or clear, with NULL) the dynamic-property hooks of `obj`. */
213
+ xt_value xt_object_set_hooks(xt_value obj, const xt_object_hooks *hooks, void *data);
188
214
  /** The shared `Object.prototype` object (root of the ordinary object chain). */
189
215
  xt_value xt_object_prototype(void);
190
216
  /** The shared `Function.prototype` object. */
@@ -250,6 +276,23 @@ xt_value xt_string_static(xt_value name, int32_t argc, xt_value *argv);
250
276
  xt_value xt_typed_array_ctor_by_name(xt_value name, int32_t argc, xt_value *argv);
251
277
  xt_value xt_typed_array_static_by_name(xt_value name, xt_value method, int32_t argc, xt_value *argv);
252
278
  xt_value xt_typed_array_prototype(xt_value name);
279
+ /** Bulk access to the bytes an `ArrayBuffer` (or a typed array's backing
280
+ * store) holds, for decoders and encoders that cannot afford one property read
281
+ * per byte. `offset` and `count` are clamped to the buffer and the number of
282
+ * bytes actually transferred is returned. Extensions pass the backing buffer
283
+ * plus a view's `byteOffset`/`byteLength`; see `runtime/ext_gui/audio.cpp`. */
284
+ uint32_t xt_array_buffer_copy_bytes_out(xt_value buffer, uint32_t offset, uint8_t *dst,
285
+ uint32_t count);
286
+ uint32_t xt_array_buffer_copy_bytes_in(xt_value buffer, uint32_t offset, const uint8_t *src,
287
+ uint32_t count);
288
+ /** Allocate a zero-filled `ArrayBuffer` of `byte_length` bytes (the native
289
+ * counterpart of `new ArrayBuffer(n)`). */
290
+ xt_value xt_array_buffer_alloc(uint32_t byte_length);
291
+ /** Typed-array view internals for extensions: whether `value` is a view, the
292
+ * `ArrayBuffer` it views, and the byte offset of the view inside it. */
293
+ int xt_is_typed_array(xt_value value);
294
+ xt_value xt_typed_array_backing_buffer(xt_value value);
295
+ uint32_t xt_typed_array_view_offset(xt_value value);
253
296
  /** Error-family construction and prototype lookup by constructor name. */
254
297
  xt_value xt_error_ctor_by_name(xt_value name, int32_t argc, xt_value *argv);
255
298
  xt_value xt_error_prototype(xt_value name);
@@ -141,6 +141,10 @@ typedef struct {
141
141
  * Deliberately not a property: `Object.keys`, `for...in` and JSON.stringify
142
142
  * must not see it, while `error.stack` returns it through `xt_get`. */
143
143
  xt_value error_stack;
144
+ /* Optional dynamic-property view (host objects such as `element.style`); see
145
+ * `xt_object_hooks` in `rt.h`. NULL for ordinary objects. */
146
+ const xt_object_hooks *hooks;
147
+ void *hook_data;
144
148
  } xt_object;
145
149
 
146
150
  typedef struct {
@@ -60,6 +60,8 @@ xt_value xt_array_buffer_ctor(int32_t argc, xt_value *argv) {
60
60
  return buffer;
61
61
  }
62
62
 
63
+ xt_value xt_array_buffer_alloc(uint32_t byte_length) { return xt_ab_new(byte_length); }
64
+
63
65
  int xt_is_array_buffer(xt_value value) { return xt_ab_is_buffer(value); }
64
66
 
65
67
  uint32_t xt_array_buffer_byte_length(xt_value value) {
@@ -79,6 +81,34 @@ void xt_array_buffer_set_byte(xt_value buffer, uint32_t index, uint8_t value) {
79
81
  xt_set(data, xt_number((double)index), xt_number((double)value));
80
82
  }
81
83
 
84
+ /* Bulk byte access. A decoder (WAV, image, …) would otherwise pay one hidden
85
+ * property read per byte, which is far too slow for a multi-megabyte file. The
86
+ * backing store is an `xt_array` of numbers; reading its `items` directly is
87
+ * the same representation `xt_ab_new` writes. */
88
+ uint32_t xt_array_buffer_copy_bytes_out(xt_value buffer, uint32_t offset, uint8_t *dst,
89
+ uint32_t count) {
90
+ xt_array *data = xt_as_array(xt_ab_data(buffer));
91
+ if (data == NULL || dst == NULL) return 0;
92
+ if (offset >= data->length) return 0;
93
+ if (count > data->length - offset) count = data->length - offset;
94
+ for (uint32_t i = 0; i < count; i++) {
95
+ dst[i] = (uint8_t)(int32_t)xt_to_number(data->items[offset + i]);
96
+ }
97
+ return count;
98
+ }
99
+
100
+ uint32_t xt_array_buffer_copy_bytes_in(xt_value buffer, uint32_t offset, const uint8_t *src,
101
+ uint32_t count) {
102
+ xt_array *data = xt_as_array(xt_ab_data(buffer));
103
+ if (data == NULL || src == NULL) return 0;
104
+ if (offset >= data->length) return 0;
105
+ if (count > data->length - offset) count = data->length - offset;
106
+ for (uint32_t i = 0; i < count; i++) {
107
+ data->items[offset + i] = xt_number((double)src[i]);
108
+ }
109
+ return count;
110
+ }
111
+
82
112
  static xt_value xt_ab_slice(xt_value thisValue, xt_value env, int32_t argc, xt_value *argv) {
83
113
  (void)env;
84
114
  xt_value data = xt_ab_data(thisValue);
@@ -406,27 +406,6 @@ xt_value xt_in(xt_value key, xt_value value) {
406
406
  return XT_FALSE;
407
407
  }
408
408
 
409
- xt_value xt_delete(xt_value value, xt_value key) {
410
- if (XT_IS_OBJECT(value)) {
411
- xt_object *obj = (xt_object *)XT_GET_PTR(value);
412
- xt_value k = xt_to_property_key(key);
413
- for (uint32_t i = 0; i < obj->count; i++) {
414
- if (xt_property_key_equals(obj->properties[i].key, k)) {
415
- for (uint32_t j = i + 1; j < obj->count; j++) obj->properties[j - 1] = obj->properties[j];
416
- obj->count--;
417
- xt_object_shape_changed(obj);
418
- break;
419
- }
420
- }
421
- return XT_TRUE;
422
- }
423
- if (XT_IS_ARRAY(value)) {
424
- if (!xt_is_symbol(key)) xt_array_set(value, key, XT_UNDEFINED);
425
- return XT_TRUE;
426
- }
427
- return XT_TRUE;
428
- }
429
-
430
409
  xt_value xt_rest_args(int32_t argc, xt_value *argv, int32_t start) {
431
410
  if (start < 0) start = 0;
432
411
  int32_t count = argc - start;
@@ -105,8 +105,7 @@ static xt_value xt_array_iterator_method(void) {
105
105
  return xt_array_iterator_method_value;
106
106
  }
107
107
 
108
- xt_value xt_get(xt_value target, xt_value key) {
109
- if (target == XT_UNDEFINED || target == XT_NULL) xt_throw_read_of_nullish(target, key);
108
+ xt_value xt_get(xt_value target, xt_value key) { if (target == XT_UNDEFINED || target == XT_NULL) xt_throw_read_of_nullish(target, key);
110
109
  if (XT_IS_ARRAY(target)) {
111
110
  /* `arr.length` must read the array's length, never an element. */
112
111
  if (XT_IS_STRING(key)) {
@@ -289,6 +288,8 @@ xt_value xt_get_ic(xt_value target, xt_value key, int64_t *cache) {
289
288
  if (obj->header.kind != XT_OBJECT_KIND_OBJECT && obj->header.kind != XT_OBJECT_KIND_ERROR) {
290
289
  return xt_get(target, key);
291
290
  }
291
+ /* A host view answers keys dynamically, outside the shape cache. */
292
+ if (obj->hooks != NULL) return xt_get(target, key);
292
293
  if (!XT_IS_STRING(key) && !xt_is_symbol(key)) return xt_get(target, key);
293
294
  xt_shape *shape = obj->shape;
294
295
  uintptr_t cachedShape = (uintptr_t)cache[0];
@@ -387,3 +388,94 @@ xt_value xt_set(xt_value target, xt_value key, xt_value value) {
387
388
  }
388
389
  return value;
389
390
  }
391
+
392
+ /* ------------------------------------------------------------------------- */
393
+ /* Dynamic host-view properties */
394
+ /* ------------------------------------------------------------------------- */
395
+
396
+ xt_value xt_object_set_hooks(xt_value value, const xt_object_hooks *hooks, void *data) {
397
+ xt_object *obj = xt_as_object(value);
398
+ if (!obj) return value;
399
+ if (obj->header.kind != XT_OBJECT_KIND_OBJECT && obj->header.kind != XT_OBJECT_KIND_ERROR) {
400
+ return value;
401
+ }
402
+ obj->hooks = hooks;
403
+ obj->hook_data = data;
404
+ return value;
405
+ }
406
+
407
+ void xt_object_append_dynamic_keys(xt_object *obj, xt_value value, xt_value result) {
408
+ if (obj->hooks == NULL || obj->hooks->keys == NULL) return;
409
+ xt_value dynamic = obj->hooks->keys(value, obj->hook_data);
410
+ xt_array *list = xt_as_array(dynamic);
411
+ if (list == NULL) return;
412
+ for (uint32_t i = 0; i < list->length; i++) {
413
+ xt_value key = list->items[i];
414
+ if (!XT_IS_STRING(key)) continue;
415
+ if (xt_object_find(obj, key) != NULL) continue;
416
+ xt_array_push(result, key);
417
+ }
418
+ }
419
+ /* `delete obj[key]`: remove a real property (arrays clear the element) and let
420
+ * a host view delete its dynamic key. */
421
+ xt_value xt_delete(xt_value value, xt_value key) {
422
+ if (XT_IS_OBJECT(value)) {
423
+ xt_object *obj = (xt_object *)XT_GET_PTR(value);
424
+ xt_value k = xt_to_property_key(key);
425
+ for (uint32_t i = 0; i < obj->count; i++) {
426
+ if (xt_property_key_equals(obj->properties[i].key, k)) {
427
+ for (uint32_t j = i + 1; j < obj->count; j++) obj->properties[j - 1] = obj->properties[j];
428
+ obj->count--;
429
+ xt_object_shape_changed(obj);
430
+ break;
431
+ }
432
+ }
433
+ /* A host view (`element.style`, `element.dataset`) owns no real property for
434
+ * its keys, so `delete` must reach it even when the bag stayed empty. */
435
+ if (obj->hooks != NULL && obj->hooks->remove != NULL) {
436
+ obj->hooks->remove(value, k, obj->hook_data);
437
+ }
438
+ return XT_TRUE;
439
+ }
440
+ if (XT_IS_ARRAY(value)) {
441
+ if (!xt_is_symbol(key)) xt_array_set(value, key, XT_UNDEFINED);
442
+ return XT_TRUE;
443
+ }
444
+ return XT_TRUE;
445
+ }
446
+ xt_value xt_object_has_own(xt_value value, xt_value key) {
447
+ if (XT_IS_ARRAY(value)) {
448
+ xt_array *array = (xt_array *)XT_GET_PTR(value);
449
+ xt_value propertyKey = xt_to_property_key(key);
450
+ if (XT_IS_STRING(propertyKey)) {
451
+ xt_string *k = xt_as_string(propertyKey);
452
+ if (k->length == 6 && memcmp(xt_string_bytes(k), "length", 6) == 0) return XT_TRUE;
453
+ uint32_t index = 0;
454
+ if (xt_key_is_array_index(k, &index)) return xt_bool(index < array->length);
455
+ }
456
+ return xt_bool(XT_IS_OBJECT(array->extra) &&
457
+ xt_object_find((xt_object *)XT_GET_PTR(array->extra), propertyKey));
458
+ }
459
+ if (XT_IS_STRING(value)) {
460
+ xt_string *string = xt_as_string(value);
461
+ if (xt_is_symbol(key)) return XT_FALSE;
462
+ xt_string *k = xt_as_string(xt_to_string(key));
463
+ if (k->length == 6 && memcmp(xt_string_bytes(k), "length", 6) == 0) return XT_TRUE;
464
+ uint32_t index = 0;
465
+ return xt_bool(xt_key_is_array_index(k, &index) && index < xt_string_utf16_length(string));
466
+ }
467
+ xt_object *obj = xt_as_object(value);
468
+ if (!obj) return XT_FALSE;
469
+ /* Map/Set/Date/RegExp/Promise/Symbol use their own struct layout and have no
470
+ * property bag; interpreting them as `xt_object` would read past the end. */
471
+ if (obj->header.kind != XT_OBJECT_KIND_OBJECT && obj->header.kind != XT_OBJECT_KIND_ERROR) {
472
+ return XT_FALSE;
473
+ }
474
+ xt_value propertyKey = xt_to_property_key(key);
475
+ if (xt_object_find(obj, propertyKey) != NULL) return XT_TRUE;
476
+ if (obj->hooks != NULL && obj->hooks->has != NULL &&
477
+ obj->hooks->has(value, propertyKey, obj->hook_data)) {
478
+ return XT_TRUE;
479
+ }
480
+ return XT_FALSE;
481
+ }
@@ -170,6 +170,9 @@ xt_value xt_object_new_with_proto(xt_value proto) {
170
170
  obj->prototype = proto;
171
171
  obj->frozen = 0;
172
172
  obj->is_array_base = 0;
173
+ obj->error_stack = XT_UNDEFINED;
174
+ obj->hooks = NULL;
175
+ obj->hook_data = NULL;
173
176
  return XT_FROM_PTR(XT_TAG_OBJECT, obj);
174
177
  }
175
178
 
@@ -191,6 +194,10 @@ static xt_property *xt_object_find(xt_object *obj, xt_value key) {
191
194
  return NULL;
192
195
  }
193
196
 
197
+ /* Defined in `access.inc`: append the dynamic keys of a host view whose
198
+ * `xt_object_hooks::keys` returns them. */
199
+ static void xt_object_append_dynamic_keys(xt_object *obj, xt_value value, xt_value result);
200
+
194
201
  /*
195
202
  * JavaScript enumerates own string keys in a canonical order: array-index
196
203
  * keys (the canonical decimal form of an integer in [0, 2^32 - 2]) first, in
@@ -313,6 +320,13 @@ xt_value xt_object_get(xt_value value, xt_value key) {
313
320
  if (!XT_IS_OBJECT(cur->prototype)) break;
314
321
  cur = (xt_object *)XT_GET_PTR(cur->prototype);
315
322
  }
323
+ /* A host view (`element.style`, `element.dataset`, computed style) answers the
324
+ * keys it owns dynamically. Only a miss reaches this point, so prototype
325
+ * methods and real properties win. */
326
+ if (obj->hooks != NULL && obj->hooks->get != NULL) {
327
+ xt_value dynamic = obj->hooks->get(value, k, obj->hook_data);
328
+ if (!XT_IS_UNDEFINED(dynamic)) return dynamic;
329
+ }
316
330
  return XT_UNDEFINED;
317
331
  }
318
332
 
@@ -368,6 +382,11 @@ xt_value xt_object_set(xt_value value, xt_value key, xt_value newValue) {
368
382
  }
369
383
  ancestor = xt_object_prototype_of(ancestor->prototype, ancestor);
370
384
  }
385
+ /* A host view handles the assignment itself (e.g. `style.width = "50%"` or
386
+ * `dataset.foo = "1"`); it never stores a shadowing data property. */
387
+ if (obj->hooks != NULL && obj->hooks->set != NULL) {
388
+ if (obj->hooks->set(value, k, newValue, obj->hook_data)) return newValue;
389
+ }
371
390
  xt_property *inserted = xt_object_insert_property(obj, k);
372
391
  inserted->value = newValue;
373
392
  return newValue;
@@ -446,36 +465,6 @@ int xt_object_is_frozen(xt_value value) {
446
465
  return obj ? obj->frozen : 0;
447
466
  }
448
467
 
449
- xt_value xt_object_has_own(xt_value value, xt_value key) {
450
- if (XT_IS_ARRAY(value)) {
451
- xt_array *array = (xt_array *)XT_GET_PTR(value);
452
- xt_value propertyKey = xt_to_property_key(key);
453
- if (XT_IS_STRING(propertyKey)) {
454
- xt_string *k = xt_as_string(propertyKey);
455
- if (k->length == 6 && memcmp(xt_string_bytes(k), "length", 6) == 0) return XT_TRUE;
456
- uint32_t index = 0;
457
- if (xt_key_is_array_index(k, &index)) return xt_bool(index < array->length);
458
- }
459
- return xt_bool(XT_IS_OBJECT(array->extra) &&
460
- xt_object_find((xt_object *)XT_GET_PTR(array->extra), propertyKey));
461
- }
462
- if (XT_IS_STRING(value)) {
463
- xt_string *string = xt_as_string(value);
464
- if (xt_is_symbol(key)) return XT_FALSE;
465
- xt_string *k = xt_as_string(xt_to_string(key));
466
- if (k->length == 6 && memcmp(xt_string_bytes(k), "length", 6) == 0) return XT_TRUE;
467
- uint32_t index = 0;
468
- return xt_bool(xt_key_is_array_index(k, &index) && index < xt_string_utf16_length(string));
469
- }
470
- xt_object *obj = xt_as_object(value);
471
- if (!obj) return XT_FALSE;
472
- /* Map/Set/Date/RegExp/Promise/Symbol use their own struct layout and have no
473
- * property bag; interpreting them as `xt_object` would read past the end. */
474
- if (obj->header.kind != XT_OBJECT_KIND_OBJECT && obj->header.kind != XT_OBJECT_KIND_ERROR) {
475
- return XT_FALSE;
476
- }
477
- return xt_bool(xt_object_find(obj, xt_to_property_key(key)) != NULL);
478
- }
479
468
 
480
469
  xt_value xt_object_from_entries(xt_value entries) {
481
470
  xt_value result = xt_object_new();
@@ -495,7 +484,13 @@ xt_value xt_object_has(xt_value value, xt_value key) {
495
484
  if (obj->header.kind != XT_OBJECT_KIND_OBJECT && obj->header.kind != XT_OBJECT_KIND_ERROR) {
496
485
  return XT_FALSE;
497
486
  }
498
- return xt_bool(xt_object_find(obj, xt_to_property_key(key)) != NULL);
487
+ xt_value propertyKey = xt_to_property_key(key);
488
+ if (xt_object_find(obj, propertyKey) != NULL) return XT_TRUE;
489
+ if (obj->hooks != NULL && obj->hooks->has != NULL &&
490
+ obj->hooks->has(value, propertyKey, obj->hook_data)) {
491
+ return XT_TRUE;
492
+ }
493
+ return XT_FALSE;
499
494
  }
500
495
 
501
496
  /* `Object.getOwnPropertySymbols`: the symbol-keyed own properties, in
@@ -531,6 +526,9 @@ xt_value xt_object_keys(xt_value value) {
531
526
  for (uint32_t i = 0; i < obj->count; i++) {
532
527
  if (XT_IS_STRING(obj->properties[i].key)) xt_array_push(result, obj->properties[i].key);
533
528
  }
529
+ /* Dynamic keys of a host view (`element.dataset`, ...), appended without
530
+ * repeating a real property that was just pushed. */
531
+ xt_object_append_dynamic_keys(obj, value, result);
534
532
  return result;
535
533
  }
536
534
  if (XT_IS_ARRAY(value)) {