1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2 * vim: set ts=8 sts=2 et sw=2 tw=80:
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "vm/ArrayBufferViewObject.h"
9 #include "builtin/DataViewObject.h"
10 #include "gc/Nursery.h"
11 #include "js/ErrorReport.h"
12 #include "js/experimental/TypedData.h" // JS_GetArrayBufferView{Data,Buffer,Length,ByteOffset}, JS_GetObjectAsArrayBufferView, JS_IsArrayBufferViewObject
13 #include "js/SharedArrayBuffer.h"
14 #include "vm/Compartment.h"
15 #include "vm/JSContext.h"
16 #include "vm/TypedArrayObject.h"
18 #include "gc/Nursery-inl.h"
19 #include "vm/ArrayBufferObject-inl.h"
20 #include "vm/NativeObject-inl.h"
24 // This method is used to trace TypedArrayObjects and DataViewObjects. It
25 // updates the object's data pointer if it points to inline data in an object
28 void ArrayBufferViewObject::trace(JSTracer
* trc
, JSObject
* obj
) {
29 ArrayBufferViewObject
* view
= &obj
->as
<ArrayBufferViewObject
>();
31 // Update view's data pointer if it moved.
32 if (view
->hasBuffer()) {
33 JSObject
* bufferObj
= &view
->bufferValue().toObject();
34 ArrayBufferObject
* buffer
= nullptr;
35 if (gc::MaybeForwardedObjectIs
<FixedLengthArrayBufferObject
>(bufferObj
)) {
37 &gc::MaybeForwardedObjectAs
<FixedLengthArrayBufferObject
>(bufferObj
);
38 } else if (gc::MaybeForwardedObjectIs
<ResizableArrayBufferObject
>(
41 &gc::MaybeForwardedObjectAs
<ResizableArrayBufferObject
>(bufferObj
);
44 size_t offset
= view
->dataPointerOffset();
45 MOZ_ASSERT_IF(!buffer
->dataPointer(), offset
== 0);
47 // The data may or may not be inline with the buffer. The buffer can only
48 // move during a compacting GC, in which case its objectMoved hook has
49 // already updated the buffer's data pointer.
50 view
->notifyBufferMoved(
51 static_cast<uint8_t*>(view
->dataPointerEither_()) - offset
,
52 buffer
->dataPointer());
58 bool JSObject::is
<js::ArrayBufferViewObject
>() const {
59 return is
<DataViewObject
>() || is
<TypedArrayObject
>();
62 void ArrayBufferViewObject::notifyBufferDetached() {
63 MOZ_ASSERT(!isSharedMemory());
64 MOZ_ASSERT(hasBuffer());
65 MOZ_ASSERT(!bufferUnshared()->isLengthPinned());
67 setFixedSlot(LENGTH_SLOT
, PrivateValue(size_t(0)));
68 setFixedSlot(BYTEOFFSET_SLOT
, PrivateValue(size_t(0)));
69 setFixedSlot(DATA_SLOT
, UndefinedValue());
72 void ArrayBufferViewObject::notifyBufferResized() {
73 MOZ_ASSERT(!isSharedMemory());
74 MOZ_ASSERT(hasBuffer());
75 MOZ_ASSERT(!bufferUnshared()->isLengthPinned());
76 MOZ_ASSERT(bufferUnshared()->isResizable());
78 computeResizableLengthAndByteOffset(bytesPerElement());
81 void ArrayBufferViewObject::notifyBufferMoved(uint8_t* srcBufStart
,
82 uint8_t* dstBufStart
) {
83 MOZ_ASSERT(!isSharedMemory());
84 MOZ_ASSERT(hasBuffer());
86 if (srcBufStart
!= dstBufStart
) {
87 void* data
= dstBufStart
+ dataPointerOffset();
88 getFixedSlotRef(DATA_SLOT
).unbarrieredSet(PrivateValue(data
));
93 bool ArrayBufferViewObject::ensureNonInline(
94 JSContext
* cx
, Handle
<ArrayBufferViewObject
*> view
) {
95 MOZ_ASSERT(!view
->isSharedMemory());
96 // Create an ArrayBuffer for the data if it was in the view.
97 ArrayBufferObjectMaybeShared
* buffer
= ensureBufferObject(cx
, view
);
101 Rooted
<ArrayBufferObject
*> unsharedBuffer(cx
,
102 &buffer
->as
<ArrayBufferObject
>());
103 return ArrayBufferObject::ensureNonInline(cx
, unsharedBuffer
);
107 ArrayBufferObjectMaybeShared
* ArrayBufferViewObject::ensureBufferObject(
108 JSContext
* cx
, Handle
<ArrayBufferViewObject
*> thisObject
) {
109 if (thisObject
->is
<TypedArrayObject
>()) {
110 Rooted
<TypedArrayObject
*> typedArray(cx
,
111 &thisObject
->as
<TypedArrayObject
>());
112 if (!TypedArrayObject::ensureHasBuffer(cx
, typedArray
)) {
116 auto* buffer
= thisObject
->bufferEither();
118 MOZ_DIAGNOSTIC_ASSERT(!cx
->brittleMode
, "ABV has no buffer");
123 bool ArrayBufferViewObject::init(JSContext
* cx
,
124 ArrayBufferObjectMaybeShared
* buffer
,
125 size_t byteOffset
, size_t length
,
126 uint32_t bytesPerElement
) {
127 MOZ_ASSERT_IF(!buffer
, byteOffset
== 0);
128 MOZ_ASSERT_IF(buffer
, !buffer
->isDetached());
130 MOZ_ASSERT(byteOffset
<= ArrayBufferObject::ByteLengthLimit
);
131 MOZ_ASSERT(length
<= ArrayBufferObject::ByteLengthLimit
);
132 MOZ_ASSERT(byteOffset
+ length
<= ArrayBufferObject::ByteLengthLimit
);
134 MOZ_ASSERT_IF(is
<TypedArrayObject
>(),
135 length
<= TypedArrayObject::ByteLengthLimit
/ bytesPerElement
);
137 // The isSharedMemory property is invariant. Self-hosting code that
138 // sets BUFFER_SLOT or the private slot (if it does) must maintain it by
139 // always setting those to reference shared memory.
140 if (buffer
&& buffer
->is
<SharedArrayBufferObject
>()) {
144 initFixedSlot(BYTEOFFSET_SLOT
, PrivateValue(byteOffset
));
145 initFixedSlot(LENGTH_SLOT
, PrivateValue(length
));
147 initFixedSlot(BUFFER_SLOT
, ObjectValue(*buffer
));
149 MOZ_ASSERT(!isSharedMemory());
150 initFixedSlot(BUFFER_SLOT
, JS::FalseValue());
154 SharedMem
<uint8_t*> ptr
= buffer
->dataPointerEither();
155 initDataPointer(ptr
+ byteOffset
);
157 // Only ArrayBuffers used for inline typed objects can have
158 // nursery-allocated data and we shouldn't see such buffers here.
159 MOZ_ASSERT_IF(buffer
->byteLength() > 0, !cx
->nursery().isInside(ptr
));
161 MOZ_ASSERT(is
<FixedLengthTypedArrayObject
>());
162 MOZ_ASSERT(length
* bytesPerElement
<=
163 FixedLengthTypedArrayObject::INLINE_BUFFER_LIMIT
);
164 void* data
= fixedData(FixedLengthTypedArrayObject::FIXED_DATA_START
);
165 initReservedSlot(DATA_SLOT
, PrivateValue(data
));
166 memset(data
, 0, length
* bytesPerElement
);
169 uint8_t* elements
= static_cast<uint8_t*>(data
);
170 elements
[0] = ZeroLengthArrayData
;
177 size_t viewByteLength
= length
* bytesPerElement
;
178 size_t viewByteOffset
= byteOffset
;
179 size_t bufferByteLength
= buffer
->byteLength();
180 // Unwraps are safe: both are for the pointer value.
181 MOZ_ASSERT_IF(buffer
->is
<ArrayBufferObject
>(),
182 buffer
->dataPointerEither().unwrap(/*safe*/) <=
183 dataPointerEither().unwrap(/*safe*/));
184 MOZ_ASSERT(bufferByteLength
- viewByteOffset
>= viewByteLength
);
185 MOZ_ASSERT(viewByteOffset
<= bufferByteLength
);
189 // ArrayBufferObjects track their views to support detaching.
190 if (buffer
&& buffer
->is
<ArrayBufferObject
>()) {
191 if (!buffer
->as
<ArrayBufferObject
>().addView(cx
, this)) {
199 bool ArrayBufferViewObject::initResizable(JSContext
* cx
,
200 ArrayBufferObjectMaybeShared
* buffer
,
201 size_t byteOffset
, size_t length
,
202 uint32_t bytesPerElement
,
203 AutoLength autoLength
) {
204 MOZ_ASSERT(buffer
->isResizable());
206 if (!init(cx
, buffer
, byteOffset
, length
, bytesPerElement
)) {
210 initFixedSlot(AUTO_LENGTH_SLOT
, BooleanValue(static_cast<bool>(autoLength
)));
211 initFixedSlot(INITIAL_LENGTH_SLOT
, PrivateValue(length
));
212 initFixedSlot(INITIAL_BYTE_OFFSET_SLOT
, PrivateValue(byteOffset
));
214 // Compute the actual byteLength and byteOffset for non-shared buffers.
215 if (!isSharedMemory()) {
216 computeResizableLengthAndByteOffset(bytesPerElement
);
219 MOZ_ASSERT(!isOutOfBounds(), "can't create out-of-bounds views");
224 void ArrayBufferViewObject::computeResizableLengthAndByteOffset(
225 size_t bytesPerElement
) {
226 MOZ_ASSERT(!isSharedMemory());
227 MOZ_ASSERT(hasBuffer());
228 MOZ_ASSERT(bufferUnshared()->isResizable());
230 size_t byteOffsetStart
= initialByteOffset();
231 size_t bufferByteLength
= bufferUnshared()->byteLength();
233 // Out-of-bounds if the byteOffset exceeds the buffer length.
234 if (byteOffsetStart
> bufferByteLength
) {
235 setFixedSlot(LENGTH_SLOT
, PrivateValue(size_t(0)));
236 setFixedSlot(BYTEOFFSET_SLOT
, PrivateValue(size_t(0)));
241 if (isAutoLength()) {
242 length
= (bufferByteLength
- byteOffsetStart
) / bytesPerElement
;
244 length
= initialLength();
246 // Out-of-bounds if the byteOffset end index exceeds the buffer length.
247 size_t byteOffsetEnd
= byteOffsetStart
+ length
* bytesPerElement
;
248 if (byteOffsetEnd
> bufferByteLength
) {
249 setFixedSlot(LENGTH_SLOT
, PrivateValue(size_t(0)));
250 setFixedSlot(BYTEOFFSET_SLOT
, PrivateValue(size_t(0)));
255 setFixedSlot(LENGTH_SLOT
, PrivateValue(length
));
256 setFixedSlot(BYTEOFFSET_SLOT
, PrivateValue(byteOffsetStart
));
259 size_t ArrayBufferViewObject::bytesPerElement() const {
260 if (is
<TypedArrayObject
>()) {
261 return as
<TypedArrayObject
>().bytesPerElement();
264 MOZ_ASSERT(is
<DataViewObject
>());
268 bool ArrayBufferViewObject::hasResizableBuffer() const {
269 if (auto* buffer
= bufferEither()) {
270 return buffer
->isResizable();
275 size_t ArrayBufferViewObject::dataPointerOffset() const {
276 // Views without a buffer have a zero offset.
278 MOZ_ASSERT(byteOffsetSlotValue() == 0);
282 // Views on shared buffers store the offset in |byteOffset|.
283 if (isSharedMemory()) {
284 return byteOffsetSlotValue();
287 // Can be called during tracing, so the buffer is possibly forwarded.
288 const auto* bufferObj
= gc::MaybeForwarded(&bufferValue().toObject());
290 // Two distinct classes are used for non-shared buffers.
292 gc::MaybeForwardedObjectIs
<FixedLengthArrayBufferObject
>(bufferObj
) ||
293 gc::MaybeForwardedObjectIs
<ResizableArrayBufferObject
>(bufferObj
));
295 // Ensure these two classes can be casted to ArrayBufferObject.
297 std::is_base_of_v
<ArrayBufferObject
, FixedLengthArrayBufferObject
>);
299 std::is_base_of_v
<ArrayBufferObject
, ResizableArrayBufferObject
>);
301 // Manual cast necessary because the buffer is possibly forwarded.
302 const auto* buffer
= static_cast<const ArrayBufferObject
*>(bufferObj
);
304 // Views on resizable buffers store the offset in |initialByteOffset|.
305 if (buffer
->isResizable() && !buffer
->isDetached()) {
306 return initialByteOffsetValue();
309 // Callers expect that this method returns zero for detached buffers.
310 MOZ_ASSERT_IF(buffer
->isDetached(), byteOffsetSlotValue() == 0);
312 // Views on fixed-length buffers store the offset in |byteOffset|.
313 return byteOffsetSlotValue();
316 mozilla::Maybe
<size_t> ArrayBufferViewObject::byteOffset() const {
317 // |byteOffset| is set to zero for detached or out-of-bounds views, so a
318 // non-zero value indicates the view is in-bounds.
319 size_t byteOffset
= byteOffsetSlotValue();
320 if (byteOffset
> 0) {
321 MOZ_ASSERT(!hasDetachedBuffer());
322 MOZ_ASSERT_IF(hasResizableBuffer(), !isOutOfBounds());
323 return mozilla::Some(byteOffset
);
325 if (hasDetachedBufferOrIsOutOfBounds()) {
326 return mozilla::Nothing
{};
328 return mozilla::Some(0);
331 mozilla::Maybe
<size_t> ArrayBufferViewObject::length() const {
332 // |length| is set to zero for detached or out-of-bounds views, so a non-zero
333 // value indicates the view is in-bounds.
334 size_t length
= lengthSlotValue();
335 if (MOZ_LIKELY(length
> 0)) {
336 MOZ_ASSERT(!hasDetachedBuffer());
337 MOZ_ASSERT_IF(hasResizableBuffer(), !isOutOfBounds());
338 MOZ_ASSERT(!isSharedMemory() || !hasResizableBuffer() || !isAutoLength(),
339 "length is zero for auto-length growable shared buffers");
340 return mozilla::Some(length
);
343 if (hasDetachedBufferOrIsOutOfBounds()) {
344 return mozilla::Nothing
{};
347 if (isSharedMemory()) {
348 auto* buffer
= bufferShared();
349 MOZ_ASSERT(buffer
, "shared memory doesn't use inline data");
351 // Views backed by a growable SharedArrayBuffer can never get out-of-bounds,
352 // but we have to dynamically compute the length when the auto-length flag
354 if (buffer
->isGrowable() && isAutoLength()) {
355 size_t bufferByteLength
= buffer
->byteLength();
356 size_t byteOffset
= byteOffsetSlotValue();
357 MOZ_ASSERT(byteOffset
<= bufferByteLength
);
358 MOZ_ASSERT(byteOffset
== initialByteOffset(),
359 "views on growable shared buffers can't get out-of-bounds");
361 return mozilla::Some((bufferByteLength
- byteOffset
) / bytesPerElement());
364 return mozilla::Some(0);
367 #if defined(DEBUG) || defined(JS_JITSPEW)
368 void ArrayBufferViewObject::dumpOwnFields(js::JSONPrinter
& json
) const {
369 json
.formatProperty("length", "%zu",
370 size_t(getFixedSlot(LENGTH_SLOT
).toPrivate()));
371 json
.formatProperty("byteOffset", "%zu",
372 size_t(getFixedSlot(BYTEOFFSET_SLOT
).toPrivate()));
373 void* data
= dataPointerEither_();
375 json
.formatProperty("data", "0x%p", data
);
377 json
.nullProperty("data");
381 void ArrayBufferViewObject::dumpOwnStringContent(
382 js::GenericPrinter
& out
) const {
383 out
.printf("length=%zu, byteOffset=%zu, ",
384 size_t(getFixedSlot(LENGTH_SLOT
).toPrivate()),
385 size_t(getFixedSlot(BYTEOFFSET_SLOT
).toPrivate()));
386 void* data
= dataPointerEither_();
388 out
.printf("data=0x%p", data
);
390 out
.put("data=null");
397 JS_PUBLIC_API
bool JS_IsArrayBufferViewObject(JSObject
* obj
) {
398 return obj
->canUnwrapAs
<ArrayBufferViewObject
>();
401 JS_PUBLIC_API JSObject
* js::UnwrapArrayBufferView(JSObject
* obj
) {
402 return obj
->maybeUnwrapIf
<ArrayBufferViewObject
>();
405 JS_PUBLIC_API
void* JS_GetArrayBufferViewData(JSObject
* obj
,
406 bool* isSharedMemory
,
407 const JS::AutoRequireNoGC
&) {
408 ArrayBufferViewObject
* view
= obj
->maybeUnwrapAs
<ArrayBufferViewObject
>();
413 *isSharedMemory
= view
->isSharedMemory();
414 return view
->dataPointerEither().unwrap(
415 /*safe - caller sees isSharedMemory flag*/);
418 JS_PUBLIC_API
uint8_t* JS_GetArrayBufferViewFixedData(JSObject
* obj
,
421 ArrayBufferViewObject
* view
= obj
->maybeUnwrapAs
<ArrayBufferViewObject
>();
426 // Disallow shared memory until it is needed.
427 if (view
->isSharedMemory()) {
431 // TypedArrays (but not DataViews) can have inline data, in which case we
432 // need to copy into the given buffer.
433 if (view
->is
<FixedLengthTypedArrayObject
>()) {
434 auto* ta
= &view
->as
<FixedLengthTypedArrayObject
>();
435 if (ta
->hasInlineElements()) {
436 size_t bytes
= ta
->byteLength();
437 if (bytes
> bufSize
) {
438 return nullptr; // Does not fit.
440 memcpy(buffer
, view
->dataPointerUnshared(), bytes
);
445 return static_cast<uint8_t*>(view
->dataPointerUnshared());
448 JS_PUBLIC_API JSObject
* JS_GetArrayBufferViewBuffer(JSContext
* cx
,
450 bool* isSharedMemory
) {
455 Rooted
<ArrayBufferViewObject
*> unwrappedView(
456 cx
, obj
->maybeUnwrapAs
<ArrayBufferViewObject
>());
457 if (!unwrappedView
) {
458 MOZ_DIAGNOSTIC_ASSERT(!cx
->brittleMode
, "access to buffer denied");
459 ReportAccessDenied(cx
);
463 ArrayBufferObjectMaybeShared
* unwrappedBuffer
;
465 AutoRealm
ar(cx
, unwrappedView
);
467 ArrayBufferViewObject::ensureBufferObject(cx
, unwrappedView
);
468 if (!unwrappedBuffer
) {
472 *isSharedMemory
= unwrappedBuffer
->is
<SharedArrayBufferObject
>();
474 RootedObject
buffer(cx
, unwrappedBuffer
);
475 if (!cx
->compartment()->wrap(cx
, &buffer
)) {
476 MOZ_DIAGNOSTIC_ASSERT(!cx
->brittleMode
, "wrapping buffer failed");
483 JS_PUBLIC_API
size_t JS_GetArrayBufferViewByteLength(JSObject
* obj
) {
484 obj
= obj
->maybeUnwrapAs
<ArrayBufferViewObject
>();
488 size_t length
= obj
->is
<DataViewObject
>()
489 ? obj
->as
<DataViewObject
>().byteLength().valueOr(0)
490 : obj
->as
<TypedArrayObject
>().byteLength().valueOr(0);
494 bool JS::ArrayBufferView::isDetached() const {
496 return obj
->as
<ArrayBufferViewObject
>().hasDetachedBuffer();
499 bool JS::ArrayBufferView::isResizable() const {
501 return obj
->as
<ArrayBufferViewObject
>().hasResizableBuffer();
504 JS_PUBLIC_API
size_t JS_GetArrayBufferViewByteOffset(JSObject
* obj
) {
505 obj
= obj
->maybeUnwrapAs
<ArrayBufferViewObject
>();
509 size_t offset
= obj
->is
<DataViewObject
>()
510 ? obj
->as
<DataViewObject
>().byteOffset().valueOr(0)
511 : obj
->as
<TypedArrayObject
>().byteOffset().valueOr(0);
515 JS_PUBLIC_API
mozilla::Span
<uint8_t> JS::ArrayBufferView::getData(
516 bool* isSharedMemory
, const AutoRequireNoGC
&) {
517 MOZ_ASSERT(obj
->is
<ArrayBufferViewObject
>());
518 size_t byteLength
= obj
->is
<DataViewObject
>()
519 ? obj
->as
<DataViewObject
>().byteLength().valueOr(0)
520 : obj
->as
<TypedArrayObject
>().byteLength().valueOr(0);
521 ArrayBufferViewObject
& view
= obj
->as
<ArrayBufferViewObject
>();
522 *isSharedMemory
= view
.isSharedMemory();
523 return {static_cast<uint8_t*>(view
.dataPointerEither().unwrap(
524 /*safe - caller sees isShared flag*/)),
528 JS_PUBLIC_API JSObject
* JS_GetObjectAsArrayBufferView(JSObject
* obj
,
530 bool* isSharedMemory
,
532 obj
= obj
->maybeUnwrapIf
<ArrayBufferViewObject
>();
537 js::GetArrayBufferViewLengthAndData(obj
, length
, isSharedMemory
, data
);
541 JS_PUBLIC_API
void js::GetArrayBufferViewLengthAndData(JSObject
* obj
,
543 bool* isSharedMemory
,
545 JS::AutoAssertNoGC nogc
;
547 JS::ArrayBufferView::fromObject(obj
).getData(isSharedMemory
, nogc
);
549 *length
= span
.Length();
552 JS_PUBLIC_API
bool JS::IsArrayBufferViewShared(JSObject
* obj
) {
553 ArrayBufferViewObject
* view
= obj
->maybeUnwrapAs
<ArrayBufferViewObject
>();
557 return view
->isSharedMemory();
560 JS_PUBLIC_API
bool JS::IsLargeArrayBufferView(JSObject
* obj
) {
562 obj
= &obj
->unwrapAs
<ArrayBufferViewObject
>();
563 size_t len
= obj
->is
<DataViewObject
>()
564 ? obj
->as
<DataViewObject
>().byteLength().valueOr(0)
565 : obj
->as
<TypedArrayObject
>().byteLength().valueOr(0);
566 return len
> ArrayBufferObject::ByteLengthLimitForSmallBuffer
;
568 // Large ArrayBuffers are not supported on 32-bit.
569 static_assert(ArrayBufferObject::ByteLengthLimit
==
570 ArrayBufferObject::ByteLengthLimitForSmallBuffer
);
575 JS_PUBLIC_API
bool JS::IsResizableArrayBufferView(JSObject
* obj
) {
576 auto* view
= &obj
->unwrapAs
<ArrayBufferViewObject
>();
577 if (auto* buffer
= view
->bufferEither()) {
578 return buffer
->isResizable();
583 JS_PUBLIC_API
bool JS::PinArrayBufferOrViewLength(JSObject
* obj
, bool pin
) {
584 ArrayBufferObjectMaybeShared
* buffer
=
585 obj
->maybeUnwrapIf
<ArrayBufferObjectMaybeShared
>();
587 return buffer
->pinLength(pin
);
590 ArrayBufferViewObject
* view
= obj
->maybeUnwrapAs
<ArrayBufferViewObject
>();
592 return view
->pinLength(pin
);
595 MOZ_DIAGNOSTIC_ASSERT(!js::TlsContext
.get()->brittleMode
,
596 "invalid type in PinABOVLength");
600 JS_PUBLIC_API
bool JS::EnsureNonInlineArrayBufferOrView(JSContext
* cx
,
602 if (obj
->is
<SharedArrayBufferObject
>()) {
603 // Always locked and out of line.
607 auto* buffer
= obj
->maybeUnwrapIf
<ArrayBufferObject
>();
609 Rooted
<ArrayBufferObject
*> rootedBuffer(cx
, buffer
);
610 return ArrayBufferObject::ensureNonInline(cx
, rootedBuffer
);
613 auto* view
= obj
->maybeUnwrapIf
<ArrayBufferViewObject
>();
615 if (view
->isSharedMemory()) {
616 // Always locked and out of line.
619 Rooted
<ArrayBufferViewObject
*> rootedView(cx
, view
);
620 return ArrayBufferViewObject::ensureNonInline(cx
, rootedView
);
623 MOZ_DIAGNOSTIC_ASSERT(!cx
->brittleMode
, "unhandled type");
624 JS_ReportErrorASCII(cx
, "unhandled type");