0

Add support for container of nullable primitives to js-lite

This change updates the js-lite binding to support arrays of nullables.
The change checks a bitfield in the serialized format when decoding and
encodes the same bitfield when serializing. This is only used for arrays
of nullable primitives.

The wire format is as follows:

| header | | bitfield | | padding | | values |

The bitfield dictates whether or not a value in the array is null or not.
Note that null values are still serialized (the value that is used is up
to the implementation).

Working:
C++
Js

Not working:
Java
Ts

Change-Id: I657e8ab381181dcce1829ed7c7f7e3420d84935f
Bug: 657632
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5280977
Reviewed-by: Rebekah Potter <rbpotter@chromium.org>
Reviewed-by: Yuzhu Shen <yzshen@chromium.org>
Commit-Queue: Fred Shih <ffred@chromium.org>
Reviewed-by: Daniel Cheng <dcheng@chromium.org>
Reviewed-by: Peter Beverloo <peter@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1288235}
This commit is contained in:
Fred Shih
2024-04-16 19:23:00 +00:00
committed by Chromium LUCI CQ
parent f81706c658
commit 2392b787b9
6 changed files with 734 additions and 41 deletions

@ -85,6 +85,76 @@ void Params::SendNullEnum(std::optional<mojom::RegularEnum> optional_enum,
std::move(callback).Run();
}
template <typename T>
void CheckAllNulls(const std::vector<T>& optionals) {
for (auto& optional : optionals) {
CHECK(!optional.has_value());
}
}
void Params::SendNullBools(
const std::vector<std::optional<bool>>& optional_bools,
SendNullBoolsCallback callback) {
CheckAllNulls(optional_bools);
std::move(callback).Run();
}
void Params::SendNullInt16s(
const std::vector<std::optional<int16_t>>& optional_int16s,
SendNullInt16sCallback callback) {
CheckAllNulls(optional_int16s);
std::move(callback).Run();
}
void Params::SendNullUint32s(
const std::vector<std::optional<uint32_t>>& optional_uint32s,
SendNullUint32sCallback callback) {
CheckAllNulls(optional_uint32s);
std::move(callback).Run();
}
void Params::SendNullDoubles(
const std::vector<std::optional<double>>& optional_doubles,
SendNullDoublesCallback callback) {
CheckAllNulls(optional_doubles);
std::move(callback).Run();
}
void Params::SendNullEnums(
const std::vector<std::optional<mojom::RegularEnum>>& optional_enums,
SendNullEnumsCallback callback) {
CheckAllNulls(optional_enums);
std::move(callback).Run();
}
template <typename K, typename V>
void CheckAllNulls(const base::flat_map<K, V>& values) {
for (auto& pair : values) {
CHECK(!pair.second.has_value());
}
}
void Params::SendNullBoolMap(
const base::flat_map<int32_t, std::optional<bool>>& values,
SendNullBoolMapCallback callback) {
CheckAllNulls(values);
std::move(callback).Run();
}
void Params::SendNullDoubleMap(
const base::flat_map<int32_t, std::optional<double>>& values,
SendNullDoubleMapCallback callback) {
CheckAllNulls(values);
std::move(callback).Run();
}
void Params::SendNullEnumMap(
const base::flat_map<int32_t, std::optional<mojom::RegularEnum>>& values,
SendNullEnumMapCallback callback) {
CheckAllNulls(values);
std::move(callback).Run();
}
void Params::SendOptionalBool(std::optional<bool> optional_bool,
SendOptionalBoolCallback callback) {
std::move(callback).Run(optional_bool.value());
@ -145,6 +215,76 @@ void Params::SendOptionalEnum(std::optional<mojom::RegularEnum> optional_enum,
std::move(callback).Run(optional_enum.value());
}
template <typename T>
std::vector<T> Unwrap(const std::vector<std::optional<T>>& in) {
std::vector<T> out;
for (auto e : in) {
if (e) {
out.push_back(*e);
}
}
return out;
}
void Params ::SendOptionalBools(
const std::vector<std::optional<bool>>& optional_bools,
SendOptionalBoolsCallback callback) {
std::move(callback).Run(Unwrap(optional_bools));
}
void Params ::SendOptionalInt16s(
const std::vector<std::optional<int16_t>>& optional_int16s,
SendOptionalInt16sCallback callback) {
std::move(callback).Run(Unwrap(optional_int16s));
}
void Params ::SendOptionalUint32s(
const std::vector<std::optional<uint32_t>>& optional_uint32s,
SendOptionalUint32sCallback callback) {
std::move(callback).Run(Unwrap(optional_uint32s));
}
void Params ::SendOptionalDoubles(
const std::vector<std::optional<double>>& optional_doubles,
SendOptionalDoublesCallback callback) {
std::move(callback).Run(Unwrap(optional_doubles));
}
void Params ::SendOptionalEnums(
const std::vector<std::optional<mojom::RegularEnum>>& optional_enums,
SendOptionalEnumsCallback callback) {
std::move(callback).Run(Unwrap(optional_enums));
}
template <typename K, typename V>
base::flat_map<K, V> Unwrap(const base::flat_map<K, std::optional<V>>& values) {
base::flat_map<K, V> out;
for (const auto& entry : values) {
if (entry.second) {
out.insert({entry.first, *entry.second});
}
}
return out;
}
void Params::SendOptionalBoolMap(
const base::flat_map<int32_t, std::optional<bool>>& values,
SendOptionalBoolMapCallback callback) {
std::move(callback).Run(Unwrap(values));
}
void Params::SendOptionalDoubleMap(
const base::flat_map<int32_t, std::optional<double>>& values,
SendOptionalDoubleMapCallback callback) {
std::move(callback).Run(Unwrap(values));
}
void Params::SendOptionalEnumMap(
const base::flat_map<int32_t, std::optional<mojom::RegularEnum>>& values,
SendOptionalEnumMapCallback callback) {
std::move(callback).Run(Unwrap(values));
}
void Params::SendNullStructWithOptionalNumerics(
mojom::OptionalNumericsStructPtr s,
SendNullStructWithOptionalNumericsCallback callback) {
@ -235,6 +375,41 @@ void ResponseParams::GetNullDouble(GetNullDoubleCallback callback) {
void ResponseParams::GetNullEnum(GetNullEnumCallback callback) {
std::move(callback).Run(std::nullopt);
}
void ResponseParams::GetNullBools(GetNullBoolsCallback callback) {
std::move(callback).Run(std::vector({std::optional<bool>()}));
}
void ResponseParams::GetNullInt16s(GetNullInt16sCallback callback) {
std::move(callback).Run(std::vector({std::optional<int16_t>()}));
}
void ResponseParams::GetNullUint32s(GetNullUint32sCallback callback) {
std::move(callback).Run(std::vector({std::optional<uint32_t>()}));
}
void ResponseParams::GetNullDoubles(GetNullDoublesCallback callback) {
std::move(callback).Run(std::vector({std::optional<double>()}));
}
void ResponseParams::GetNullEnums(GetNullEnumsCallback callback) {
std::move(callback).Run(std::vector({std::optional<mojom::RegularEnum>()}));
}
void ResponseParams::GetNullBoolMap(GetNullBoolMapCallback callback) {
std::move(callback).Run(
base::flat_map<int16_t, std::optional<bool>>({{0, std::nullopt}}));
}
void ResponseParams::GetNullInt32Map(GetNullInt32MapCallback callback) {
std::move(callback).Run(
base::flat_map<int16_t, std::optional<int32_t>>({{0, std::nullopt}}));
}
void ResponseParams::GetNullEnumMap(GetNullEnumMapCallback callback) {
std::move(callback).Run(
base::flat_map<int16_t, std::optional<mojom::RegularEnum>>(
{{0, std::nullopt}}));
}
void ResponseParams::GetOptionalBool(bool value,
GetOptionalBoolCallback callback) {
@ -296,6 +471,54 @@ void ResponseParams::GetOptionalEnum(mojom::RegularEnum value,
std::move(callback).Run(value);
}
template <typename T>
std::vector<std::optional<T>> WrapWithNulls(T value) {
return std::vector<std::optional<T>>({std::nullopt, value, std::nullopt});
}
void ResponseParams::GetOptionalBools(bool value,
GetOptionalBoolsCallback callback) {
std::move(callback).Run(WrapWithNulls(value));
}
void ResponseParams::GetOptionalInt16s(int16_t value,
GetOptionalInt16sCallback callback) {
std::move(callback).Run(WrapWithNulls(value));
}
void ResponseParams::GetOptionalUint32s(uint32_t value,
GetOptionalUint32sCallback callback) {
std::move(callback).Run(WrapWithNulls(value));
}
void ResponseParams::GetOptionalDoubles(double value,
GetOptionalDoublesCallback callback) {
std::move(callback).Run(WrapWithNulls(value));
}
void ResponseParams::GetOptionalEnums(mojom::RegularEnum value,
GetOptionalEnumsCallback callback) {
std::move(callback).Run(WrapWithNulls(value));
}
template <typename K, typename V>
base::flat_map<K, std::optional<V>> WrapWithNulls(K key, V value) {
return base::flat_map<K, std::optional<V>>(
{{key - 1, std::nullopt}, {key, value}, {key + 1, std::nullopt}});
}
void ResponseParams::GetOptionalBoolMap(int16_t key,
bool value,
GetOptionalBoolMapCallback callback) {
std::move(callback).Run(WrapWithNulls(key, value));
}
void ResponseParams::GetOptionalFloatMap(int16_t key,
float value,
GetOptionalFloatMapCallback callback) {
std::move(callback).Run(WrapWithNulls(key, value));
}
void ResponseParams::GetOptionalEnumMap(int16_t key,
mojom::RegularEnum value,
GetOptionalEnumMapCallback callback) {
std::move(callback).Run(WrapWithNulls(key, value));
}
void ResponseParams::GetNullStructWithOptionalNumerics(
GetNullStructWithOptionalNumericsCallback callback) {
std::move(callback).Run(nullptr);

@ -38,6 +38,31 @@ class Params : public mojom::Params {
void SendNullEnum(std::optional<mojom::RegularEnum> optional_enum,
SendNullEnumCallback callback) override;
void SendNullBools(const std::vector<std::optional<bool>>& optional_bools,
SendNullBoolsCallback callback) override;
void SendNullInt16s(
const std::vector<std::optional<int16_t>>& optional_int16s,
SendNullInt16sCallback callback) override;
void SendNullUint32s(
const std::vector<std::optional<uint32_t>>& optional_uint32s,
SendNullUint32sCallback callback) override;
void SendNullDoubles(
const std::vector<std::optional<double>>& optional_doubles,
SendNullDoublesCallback callback) override;
void SendNullEnums(
const std::vector<std::optional<mojom::RegularEnum>>& optional_enums,
SendNullEnumsCallback callback) override;
void SendNullBoolMap(
const base::flat_map<int32_t, std::optional<bool>>& values,
SendNullBoolMapCallback callback) override;
void SendNullDoubleMap(
const base::flat_map<int32_t, std::optional<double>>& values,
SendNullDoubleMapCallback callback) override;
void SendNullEnumMap(
const base::flat_map<int32_t, std::optional<mojom::RegularEnum>>& values,
SendNullEnumMapCallback callback) override;
void SendOptionalBool(std::optional<bool> optional_bool,
SendOptionalBoolCallback callback) override;
void SendOptionalUint8(std::optional<uint8_t> optional_uint8,
@ -63,6 +88,31 @@ class Params : public mojom::Params {
void SendOptionalEnum(std::optional<mojom::RegularEnum> optional_enum,
SendOptionalEnumCallback callback) override;
void SendOptionalBools(const std::vector<std::optional<bool>>& optional_bools,
SendOptionalBoolsCallback callback) override;
void SendOptionalInt16s(
const std::vector<std::optional<int16_t>>& optional_int16s,
SendOptionalInt16sCallback callback) override;
void SendOptionalUint32s(
const std::vector<std::optional<uint32_t>>& optional_uint32s,
SendOptionalUint32sCallback callback) override;
void SendOptionalDoubles(
const std::vector<std::optional<double>>& optional_doubles,
SendOptionalDoublesCallback callback) override;
void SendOptionalEnums(
const std::vector<std::optional<mojom::RegularEnum>>& optional_enums,
SendOptionalEnumsCallback callback) override;
void SendOptionalBoolMap(
const base::flat_map<int32_t, std::optional<bool>>& values,
SendOptionalBoolMapCallback callback) override;
void SendOptionalDoubleMap(
const base::flat_map<int32_t, std::optional<double>>& values,
SendOptionalDoubleMapCallback callback) override;
void SendOptionalEnumMap(
const base::flat_map<int32_t, std::optional<mojom::RegularEnum>>& values,
SendOptionalEnumMapCallback callback) override;
void SendNullStructWithOptionalNumerics(
mojom::OptionalNumericsStructPtr s,
SendNullStructWithOptionalNumericsCallback callback) override;
@ -92,6 +142,16 @@ class ResponseParams : public mojom::ResponseParams {
void GetNullDouble(GetNullDoubleCallback callback) override;
void GetNullEnum(GetNullEnumCallback callback) override;
void GetNullBools(GetNullBoolsCallback callback) override;
void GetNullInt16s(GetNullInt16sCallback callback) override;
void GetNullUint32s(GetNullUint32sCallback callback) override;
void GetNullDoubles(GetNullDoublesCallback callback) override;
void GetNullEnums(GetNullEnumsCallback callback) override;
void GetNullBoolMap(GetNullBoolMapCallback callback) override;
void GetNullInt32Map(GetNullInt32MapCallback callback) override;
void GetNullEnumMap(GetNullEnumMapCallback callback) override;
void GetOptionalBool(bool value, GetOptionalBoolCallback callback) override;
void GetOptionalUint8(uint8_t value,
GetOptionalUint8Callback callback) override;
@ -115,6 +175,26 @@ class ResponseParams : public mojom::ResponseParams {
void GetOptionalEnum(mojom::RegularEnum value,
GetOptionalEnumCallback callback) override;
void GetOptionalBools(bool value, GetOptionalBoolsCallback callback) override;
void GetOptionalInt16s(int16_t value,
GetOptionalInt16sCallback callback) override;
void GetOptionalUint32s(uint32_t value,
GetOptionalUint32sCallback callback) override;
void GetOptionalDoubles(double value,
GetOptionalDoublesCallback callback) override;
void GetOptionalEnums(mojom::RegularEnum value,
GetOptionalEnumsCallback callback) override;
void GetOptionalBoolMap(int16_t key,
bool value,
GetOptionalBoolMapCallback callback) override;
void GetOptionalFloatMap(int16_t key,
float value,
GetOptionalFloatMapCallback callback) override;
void GetOptionalEnumMap(int16_t key,
mojom::RegularEnum value,
GetOptionalEnumMapCallback callback) override;
void GetNullStructWithOptionalNumerics(
GetNullStructWithOptionalNumericsCallback callback) override;
void GetStructWithNullOptionalNumerics(

@ -57,6 +57,24 @@ interface Params {
// Runs the empty callback. CHECKs if `optional_enum` is not null.
SendNullEnum(RegularEnum? optional_enum) => ();
// Runs the empty callback. CHECKs if `optional_bools` is not null.
SendNullBools(array<bool?> optional_bools) => ();
// Runs the empty callback. CHECKs if `optional_int16s` is not null.
SendNullInt16s(array<int16?> optional_int16s) => ();
// Runs the empty callback. CHECKs if `optional_uint32s` is not null.
SendNullUint32s(array<uint32?> optional_uint32s) => ();
// Runs the empty callback. CHECKs if `optional_doubles` is not null.
SendNullDoubles(array<double?> optional_doubles) => ();
// Runs the empty callback. CHECKs if `optional_enums` is not null.
SendNullEnums(array<RegularEnum?> optional_enums) => ();
// Runs the empty callback. CHECKs if `optional_bools` is not null.
SendNullBoolMap(map<int32, bool?> values) => ();
// Runs the empty callback. CHECKs if `optional_bools` is not null.
SendNullDoubleMap(map<int32, double?> values) => ();
// Runs the empty callback. CHECKs if `optional_bools` is not null.
SendNullEnumMap(map<int32, RegularEnum?> values) => ();
// The following methods help test that JS correctly handles optional
// numerics with a value.
//
@ -85,6 +103,30 @@ interface Params {
// Runs callback with `optional_enum`.
SendOptionalEnum(RegularEnum? optional_enum) => (RegularEnum value);
// Runs callback with `optional_enum`.
SendOptionalBools(array<bool?> optional_enums) => (array<bool> values);
// Runs callback with `optional_int16`.
SendOptionalInt16s(array<int16?> optional_int16s) => (array<int16> values);
// Runs callback with `optional_uint32`.
SendOptionalUint32s(array<uint32?> optional_uint32s)
=> (array<uint32> values);
// Runs callback with `optional_double`.
SendOptionalDoubles(array<double?> optional_doubles)
=> (array<double> values);
// Runs callback with `optional_enum`.
SendOptionalEnums(array<RegularEnum?> optional_enums)
=> (array<RegularEnum> values);
// Runs callback by unwrapping all null values.
SendOptionalBoolMap(map<int32, bool?> values)
=> (map<int32, bool> values);
// Runs callback by unwrapping all null values.
SendOptionalDoubleMap(map<int32, double?> values)
=> (map<int32, double> values);
// Runs callback by unwrapping all null values.
SendOptionalEnumMap(map<int32, RegularEnum?> values)
=> (map<int32, RegularEnum> values);
// Runs a callback with null. Helps test that JS can correctly handle null
// structs with optional numerics.
SendNullStructWithOptionalNumerics(OptionalNumericsStruct? s) => ();
@ -143,6 +185,24 @@ interface ResponseParams {
// Returns a null optional enum.
GetNullEnum() => (RegularEnum? optional_value);
// Returns a list of optional bools.
GetNullBools() => (array<bool?> optional_values);
// Returns a list of optional int16s.
GetNullInt16s() => (array<int16?> optional_values);
// Returns a list of optional uint32s.
GetNullUint32s() => (array<uint32?> optional_values);
// Returns a list of optional doubles.
GetNullDoubles() => (array<double?> optional_values);
// Returns a list of optional enums.
GetNullEnums() => (array<RegularEnum?> optional_values);
// Returns a simple map with a single {0, null} entry.
GetNullBoolMap() => (map<int16, bool?> optional_values);
// Returns a simple map with a single {0, null} entry.
GetNullInt32Map() => (map<int16, int32?> optional_values);
// Returns a simple map with a single {0, null} entry.
GetNullEnumMap() => (map<int16, RegularEnum?> optional_values);
// The following methods help test that JS can correctly handle optional
// numerics in response params.
//
@ -171,6 +231,37 @@ interface ResponseParams {
// Returns |value| in an optional enum.
GetOptionalEnum(RegularEnum value) => (RegularEnum? optional_value);
// Returns |value| sandwiched between two null values. E.g.:
// [null, |value|, null].
GetOptionalBools(bool value) => (array<bool?> optional_values);
// Returns |value| sandwiched between two null values. E.g.:
// [null, |value|, null].
GetOptionalInt16s(int16 value) => (array<int16?> optional_values);
// Returns |value| sandwiched between two null values. E.g.:
// [null, |value|, null].
GetOptionalUint32s(uint32 value) => (array<uint32?> optional_values);
// Returns |value| sandwiched between two null values. E.g.:
// [null, |value|, null].
GetOptionalDoubles(double value) => (array<double?> optional_values);
// Returns |value| sandwiched between two null values. E.g.:
// [null, |value|, null].
GetOptionalEnums(RegularEnum value) => (array<RegularEnum?> optional_values);
// Returns {|key|, |value|} sandwiched between two null values. E.g.:
// {
// [key - 1]: null,
// [key]: value,
// [key +1]: value,
// }
GetOptionalBoolMap(int16 key, bool value)
=> (map<int16, bool?> optional_values);
// Same as above.
GetOptionalFloatMap(int16 key, float value)
=> (map<int16, float?> optional_values);
// Same as above.
GetOptionalEnumMap(int16 key, RegularEnum value)
=> (map<int16, RegularEnum?> optional_values);
// Runs a callback with null. Helps test that JS can correctly handle null
// structs with optional numerics.
GetNullStructWithOptionalNumerics() => (OptionalNumericsStruct? s);

@ -240,14 +240,38 @@ mojo.internal.computeUnionDimensions = function(unionSpec, nullable, value) {
*/
mojo.internal.computeInlineArraySize = function(arraySpec, value) {
if (arraySpec.elementType === mojo.internal.Bool) {
return mojo.internal.kArrayHeaderSize + ((value.length + 7) >> 3);
return mojo.internal.kArrayHeaderSize +
mojo.internal.computeHasValueBitfieldSize(arraySpec, value.length) +
((value.length + 7) >> 3);
} else {
return mojo.internal.kArrayHeaderSize +
mojo.internal.computeHasValueBitfieldSize(arraySpec, value.length) +
value.length *
arraySpec.elementType.$.arrayElementSize(!!arraySpec.elementNullable);
}
};
/**
* @param {!mojo.internal.ArraySpec} arraySpec
* @param {number} length
* @return {number} the number of bytes needed for the an array's has-value
* bitfield. If the arraySpec does not require a has-value bitfield, this
* method will return 0.
*/
mojo.internal.computeHasValueBitfieldSize = function(arraySpec, length) {
const isNullableValueType = !!arraySpec.elementNullable &&
!!arraySpec.elementType.$.isValueType;
if (!isNullableValueType) {
return 0;
}
const element_type_bytes =
arraySpec.elementType.$.arrayElementSize(/* nullable= */ true);
const element_type_bits = element_type_bytes * 8;
const needed_bits = length + element_type_bits - 1;
// >> 0 to force integer arithmetic.
return ((needed_bits/element_type_bits) >> 0) * element_type_bytes;
}
/**
* @param {!mojo.internal.ArraySpec} arraySpec
* @param {!Array|!Uint8Array} value
@ -485,10 +509,6 @@ mojo.internal.Encoder = class {
* @param {!Array|!Uint8Array} value
*/
encodeArray(arraySpec, offset, value) {
if (arraySpec.elementNullable &&
!!arraySpec.elementType.$.preventNullableElement) {
throw new Error('nullable primitive elements are not supported yet');
}
const arraySize = mojo.internal.computeInlineArraySize(arraySpec, value);
const arrayData = this.message_.allocate(arraySize);
const arrayEncoder =
@ -497,8 +517,10 @@ mojo.internal.Encoder = class {
arrayEncoder.encodeUint32(0, arraySize);
arrayEncoder.encodeUint32(4, value.length);
this.maybeEncodeHasValueBitfield(arraySpec, arrayEncoder, 8, value);
let byteOffset = 8;
let byteOffset = 8 +
mojo.internal.computeHasValueBitfieldSize(arraySpec, value.length);
if (arraySpec.elementType === mojo.internal.Bool) {
let bitOffset = 0;
for (const e of value) {
@ -518,15 +540,47 @@ mojo.internal.Encoder = class {
'non-nullable elements');
}
arraySpec.elementType.$.encodeNull(arrayEncoder, byteOffset);
} else {
arraySpec.elementType.$.encode(
e, arrayEncoder, byteOffset, 0, !!arraySpec.elementNullable);
}
arraySpec.elementType.$.encode(
e, arrayEncoder, byteOffset, 0, !!arraySpec.elementNullable);
byteOffset += arraySpec.elementType.$.arrayElementSize(
!!arraySpec.elementNullable);
}
}
}
/**
* Optionally writes a has-value bitfield to the encoder if necessary. If the
* arraySpec does not require a has-value bitfield, this method call is
* noop.
* @param {!mojo.internal.ArraySpec} arraySpec
* @param {mojo.internal.Encoder} arrayEncoder
* @param {number} startOffset
* @param {!Array|!Uint8Array} value
*/
maybeEncodeHasValueBitfield(arraySpec, arrayEncoder, startOffset, value) {
if (!arraySpec.elementNullable ||
!arraySpec.elementType.$.isValueType) {
return;
}
let bitOffset = 0;
let byteOffset = startOffset;
for (const e of value) {
if (e === null || e === undefined) {
arrayEncoder.encodeBool(byteOffset, bitOffset, false);
} else {
arrayEncoder.encodeBool(byteOffset, bitOffset, true);
}
bitOffset++;
if (bitOffset == 8) {
bitOffset = 0;
byteOffset++;
}
}
}
/**
* @param {!mojo.internal.MapSpec} mapSpec
* @param {number} offset
@ -553,7 +607,7 @@ mojo.internal.Encoder = class {
mapEncoder.encodeArray(
{
elementType: mapSpec.valueType,
elementNullable: mapSpec.valueNullable
elementNullable: mapSpec.valueNullable,
},
16, values);
}
@ -782,11 +836,6 @@ mojo.internal.Decoder = class {
* @return {Array}
*/
decodeArray(arraySpec, offset) {
if (arraySpec.elementNullable &&
!!arraySpec.elementType.$.preventNullableElement) {
throw new Error('nullable primitive elements are not supported yet');
}
const arrayOffset = this.decodeOffset(offset);
if (!arrayOffset)
return null;
@ -799,18 +848,49 @@ mojo.internal.Decoder = class {
if (!numElements)
return [];
// Nullable primitives use a bitfield to represent whether a value at a
// certain index is set. This is not needed for non-primitive or
// non-nullable types.
const isNullableValueType = !!arraySpec.elementNullable &&
arraySpec.elementType.$.isValueType;
const elementHasValue = isNullableValueType ? [] : null;
if (isNullableValueType) {
let bitfieldByte = 8;
let bitfieldBit = 0;
for (let i = 0; i < numElements; ++i) {
elementHasValue.push(
arrayDecoder.decodeBool(bitfieldByte, bitfieldBit));
bitfieldBit++;
if (bitfieldBit === 8) {
bitfieldBit = 0;
bitfieldByte++;
}
}
}
let byteOffset = 8 +
mojo.internal.computeHasValueBitfieldSize(arraySpec, numElements);
const result = [];
if (arraySpec.elementType === mojo.internal.Bool) {
for (let i = 0; i < numElements; ++i)
result.push(arrayDecoder.decodeBool(8 + (i >> 3), i % 8));
if (isNullableValueType && !elementHasValue[i]) {
result.push(null);
} else {
result.push(arrayDecoder.decodeBool(byteOffset + (i >> 3), i % 8));
}
} else {
let byteOffset = 8;
for (let i = 0; i < numElements; ++i) {
const element = arraySpec.elementType.$.decode(
if (isNullableValueType && !elementHasValue[i]) {
result.push(null);
} else {
const element = arraySpec.elementType.$.decode(
arrayDecoder, byteOffset, 0, !!arraySpec.elementNullable);
if (element === null && !arraySpec.elementNullable)
throw new Error('Received unexpected array element');
result.push(element);
if (element === null && !arraySpec.elementNullable)
throw new Error('Received unexpected array element');
result.push(element);
}
byteOffset += arraySpec.elementType.$.arrayElementSize(
!!arraySpec.elementNullable);
}
@ -1098,7 +1178,7 @@ mojo.internal.deserializeMessageHeader = function(data) {
/**
* @typedef {{
* encode: function(*, !mojo.internal.Encoder, number, number, boolean),
* encodeNull: ((function(!mojo.internal.Encoder, number))|undefined),
* encodeNull: function(!mojo.internal.Encoder, number),
* decode: function(!mojo.internal.Decoder, number, number, boolean):*,
* computeDimensions:
* ((function(*, boolean):!mojo.internal.MessageDimensions)|undefined),
@ -1108,11 +1188,8 @@ mojo.internal.deserializeMessageHeader = function(data) {
* arraySpec: (!mojo.internal.ArraySpec|undefined),
* mapSpec: (!mojo.internal.MapSpec|undefined),
* structSpec: (!mojo.internal.StructSpec|undefined),
* preventNullableElement: (boolean|undefined)
* isValueType: boolean
* }}
* TODO(ffred): preventNullableElement is used to prevent arrays of nullables
* from being used until support is fully implemented. Remove once support is
* fully added.
*/
mojo.internal.MojomTypeInfo;
@ -1213,11 +1290,17 @@ mojo.internal.Bool = {
encode: function(value, encoder, byteOffset, bitOffset, nullable) {
encoder.encodeBool(byteOffset, bitOffset, value);
},
encodeNull: function(encoder, byteOffset) {
throw new Error('encoding bool null from type is not implemented');
},
decode: function(decoder, byteOffset, bitOffset, nullable) {
return decoder.decodeBool(byteOffset, bitOffset);
},
// Bool has specialized serialize/deserialize logic to bit pack. However,
// memory allocation is still a single byte.
arrayElementSize: nullable => 1,
isValidObjectKeyType: true,
preventNullableElement: true,
isValueType: true,
},
};
@ -1230,12 +1313,15 @@ mojo.internal.Int8 = {
encode: function(value, encoder, byteOffset, bitOffset, nullable) {
encoder.encodeInt8(byteOffset, value);
},
encodeNull: function(encoder, byteOffset) {
encoder.encodeInt8(byteOffset, 0);
},
decode: function(decoder, byteOffset, bitOffset, nullable) {
return decoder.decodeInt8(byteOffset);
},
arrayElementSize: nullable => 1,
isValidObjectKeyType: true,
preventNullableElement: true,
isValueType: true,
},
};
@ -1248,12 +1334,15 @@ mojo.internal.Uint8 = {
encode: function(value, encoder, byteOffset, bitOffset, nullable) {
encoder.encodeUint8(byteOffset, value);
},
encodeNull: function(encoder, byteOffset) {
encoder.encodeUint8(byteOffset, 0);
},
decode: function(decoder, byteOffset, bitOffset, nullable) {
return decoder.decodeUint8(byteOffset);
},
arrayElementSize: nullable => 1,
isValidObjectKeyType: true,
preventNullableElement: true,
isValueType: true,
},
};
@ -1266,12 +1355,15 @@ mojo.internal.Int16 = {
encode: function(value, encoder, byteOffset, bitOffset, nullable) {
encoder.encodeInt16(byteOffset, value);
},
encodeNull: function(encoder, byteOffset) {
encoder.encodeInt16(byteOffset, 0);
},
decode: function(decoder, byteOffset, bitOffset, nullable) {
return decoder.decodeInt16(byteOffset);
},
arrayElementSize: nullable => 2,
isValidObjectKeyType: true,
preventNullableElement: true,
isValueType: true,
},
};
@ -1284,12 +1376,15 @@ mojo.internal.Uint16 = {
encode: function(value, encoder, byteOffset, bitOffset, nullable) {
encoder.encodeUint16(byteOffset, value);
},
encodeNull: function(encoder, byteOffset) {
encoder.encodeUint16(byteOffset, 0);
},
decode: function(decoder, byteOffset, bitOffset, nullable) {
return decoder.decodeUint16(byteOffset);
},
arrayElementSize: nullable => 2,
isValidObjectKeyType: true,
preventNullableElement: true,
isValueType: true,
},
};
@ -1302,12 +1397,15 @@ mojo.internal.Int32 = {
encode: function(value, encoder, byteOffset, bitOffset, nullable) {
encoder.encodeInt32(byteOffset, value);
},
encodeNull: function(encoder, byteOffset) {
encoder.encodeInt32(byteOffset, 0);
},
decode: function(decoder, byteOffset, bitOffset, nullable) {
return decoder.decodeInt32(byteOffset);
},
arrayElementSize: nullable => 4,
isValidObjectKeyType: true,
preventNullableElement: true,
isValueType: true,
},
};
@ -1320,12 +1418,15 @@ mojo.internal.Uint32 = {
encode: function(value, encoder, byteOffset, bitOffset, nullable) {
encoder.encodeUint32(byteOffset, value);
},
encodeNull: function(encoder, byteOffset) {
encoder.encodeUint32(byteOffset, 0);
},
decode: function(decoder, byteOffset, bitOffset, nullable) {
return decoder.decodeUint32(byteOffset);
},
arrayElementSize: nullable => 4,
isValidObjectKeyType: true,
preventNullableElement: true,
isValueType: true,
},
};
@ -1338,13 +1439,16 @@ mojo.internal.Int64 = {
encode: function(value, encoder, byteOffset, bitOffset, nullable) {
encoder.encodeInt64(byteOffset, value);
},
encodeNull: function(encoder, byteOffset) {
encoder.encodeInt64(byteOffset, 0);
},
decode: function(decoder, byteOffset, bitOffset, nullable) {
return decoder.decodeInt64(byteOffset);
},
arrayElementSize: nullable => 8,
// TS Compiler does not allow Object maps to have bigint keys.
isValidObjectKeyType: false,
preventNullableElement: true,
isValueType: true,
},
};
@ -1357,13 +1461,16 @@ mojo.internal.Uint64 = {
encode: function(value, encoder, byteOffset, bitOffset, nullable) {
encoder.encodeUint64(byteOffset, value);
},
encodeNull: function(encoder, byteOffset) {
encoder.encodeUint64(byteOffset, 0);
},
decode: function(decoder, byteOffset, bitOffset, nullable) {
return decoder.decodeUint64(byteOffset);
},
arrayElementSize: nullable => 8,
// TS Compiler does not allow Object maps to have bigint keys.
isValidObjectKeyType: false,
preventNullableElement: true,
isValueType: true,
},
};
@ -1376,12 +1483,15 @@ mojo.internal.Float = {
encode: function(value, encoder, byteOffset, bitOffset, nullable) {
encoder.encodeFloat(byteOffset, value);
},
encodeNull: function(encoder, byteOffset) {
encoder.encodeFloat(byteOffset, 0);
},
decode: function(decoder, byteOffset, bitOffset, nullable) {
return decoder.decodeFloat(byteOffset);
},
arrayElementSize: nullable => 4,
isValidObjectKeyType: true,
preventNullableElement: true,
isValueType: true,
},
};
@ -1394,12 +1504,15 @@ mojo.internal.Double = {
encode: function(value, encoder, byteOffset, bitOffset, nullable) {
encoder.encodeDouble(byteOffset, value);
},
encodeNull: function(encoder, byteOffset) {
encoder.encodeDouble(byteOffset, 0);
},
decode: function(decoder, byteOffset, bitOffset, nullable) {
return decoder.decodeDouble(byteOffset);
},
arrayElementSize: nullable => 8,
isValidObjectKeyType: true,
preventNullableElement: true,
isValueType: true,
},
};
@ -1420,6 +1533,7 @@ mojo.internal.Handle = {
},
arrayElementSize: nullable => 4,
isValidObjectKeyType: false,
isValueType: false,
},
};
@ -1444,6 +1558,7 @@ mojo.internal.String = {
},
arrayElementSize: nullable => 8,
isValidObjectKeyType: true,
isValueType: false,
}
};
@ -1474,6 +1589,7 @@ mojo.internal.Array = function(elementType, elementNullable) {
},
arrayElementSize: nullable => 8,
isValidObjectKeyType: false,
isValueType: false,
},
};
};
@ -1526,6 +1642,7 @@ mojo.internal.Map = function(keyType, valueType, valueNullable) {
},
arrayElementSize: nullable => 8,
isValidObjectKeyType: false,
isValueType: false,
},
};
};
@ -1541,6 +1658,7 @@ mojo.internal.Enum = function() {
// TODO: Do some sender-side error checking on the input value.
encoder.encodeUint32(byteOffset, value);
},
encodeNull: function(encoder, byteOffset) {},
decode: function(decoder, byteOffset, bitOffset, nullable) {
const value = decoder.decodeInt32(byteOffset);
// TODO: validate
@ -1548,7 +1666,7 @@ mojo.internal.Enum = function() {
},
arrayElementSize: nullable => 4,
isValidObjectKeyType: true,
preventNullableElement: true,
isValueType: true,
},
};
};
@ -1678,6 +1796,7 @@ mojo.internal.InterfaceProxy = function(type) {
},
arrayElementSize: nullable => 8,
isValidObjectKeyType: false,
isValueType: false,
},
};
};
@ -1703,6 +1822,7 @@ mojo.internal.InterfaceRequest = function(type) {
},
arrayElementSize: nullable => 8,
isValidObjectKeyType: false,
isValueType: false,
},
};
};
@ -1734,6 +1854,7 @@ mojo.internal.AssociatedInterfaceProxy = function(type) {
},
isValidObjectKeyType: false,
hasInterfaceId: true,
isValueType: false,
},
};
};
@ -1764,7 +1885,7 @@ mojo.internal.AssociatedInterfaceRequest = function(type) {
},
isValidObjectKeyType: false,
hasInterfaceId: true,
isValueType: false,
},
};
};

@ -26,6 +26,16 @@ class ParamsJsImpl {
'sendNullFloat',
'sendNullDouble',
'sendNullEnum',
'sendNullBools',
'sendNullInt16s',
'sendNullUint32s',
'sendNullDoubles',
'sendNullEnums',
'sendNullBoolMap',
'sendNullDoubleMap',
'sendNullEnumMap',
];
for (const method of sendNullMethods) {
this[method] = this.sendNull;
@ -44,6 +54,16 @@ class ParamsJsImpl {
'sendOptionalFloat',
'sendOptionalDouble',
'sendOptionalEnum',
'sendOptionalBools',
'sendOptionalInt16s',
'sendOptionalUint32s',
'sendOptionalDoubles',
'sendOptionalEnums',
'sendOptionalBoolMap',
'sendOptionalDoubleMap',
'sendOptionalEnumMap',
];
for (const method of sendOptionalMethods) {
this[method] = this.sendOptional;
@ -159,6 +179,20 @@ for (const {method, numericalType} of testNullMethods) {
}, `JS decoding of null ${numericalType} param.`);
}
promise_test(async () => {
assert_empty_response(await cpp.sendNullBools([null, null]));
assert_empty_response(await cpp.sendNullInt16s([null, null]));
assert_empty_response(await cpp.sendNullUint32s([null, null]));
assert_empty_response(await cpp.sendNullDoubles([null, null]));
assert_empty_response(await cpp.sendNullEnums([null, null]));
});
promise_test(async () => {
assert_empty_response(await cpp.sendNullBoolMap({0: null}));
assert_empty_response(await cpp.sendNullDoubleMap({1: null}));
assert_empty_response(await cpp.sendNullEnumMap({2: null}));
});
promise_test(async () => {
assert_empty_response(await cpp.sendNullStructWithOptionalNumerics(null));
}, `JS encoding and C++ decoding of null struct with optional numerics.`);
@ -225,10 +259,56 @@ for (const {method, valueToUse, numericalType} of testMethods) {
promise_test(async () => {
assert_value_equals(await cpp[method](valueToUse), valueToUse);
}, `JS encoding and C++ decoding of optional ${numericalType}.`);
}
promise_test(async () => {
assert_value_equals(await cpp[method](valueToUse), valueToUse);
}, `JS decoding of optional ${numericalType} params.`);
const testArrayMethods = [{
method: 'sendOptionalBools',
valuesToUse: [true, null, false, true],
expected: [true, false, true],
}, {
method: 'sendOptionalInt16s',
valuesToUse: [3, null, 2, 1],
expected: [3, 2, 1],
}, {
method: 'sendOptionalUint32s',
valuesToUse: [null, 1],
expected: [1],
}, {
method: 'sendOptionalDoubles',
valuesToUse: [6.66, 9.99],
expected: [6.66, 9.99],
}, {
method: 'sendOptionalEnums',
valuesToUse: [null, OptionalNumericsRegularEnum.kBar, null, null],
expected: [OptionalNumericsRegularEnum.kBar],
}];
for (const {method, valuesToUse, expected} of testArrayMethods) {
promise_test(async() => {
const response = await cpp[method](valuesToUse);
assert_array_equals(response.values, expected, `JS encoding and C++ decoding of: ${method}`);
});
}
const testMapMethods = [{
method: 'sendOptionalBoolMap',
valuesToUse: {0: true, 1: null, 2:false},
expected: {0: true, 2: false},
}, {
method: 'sendOptionalDoubleMap',
valuesToUse: {3: 3.33, 4: null},
expected: {3: 3.33},
}, {
method: 'sendOptionalEnumMap',
valuesToUse: {5: null, 6: OptionalNumericsRegularEnum.kBar},
expected: {6: OptionalNumericsRegularEnum.kBar},
}];
for (const {method, valuesToUse, expected} of testMapMethods) {
promise_test(async() => {
const response = await cpp[method](valuesToUse);
assert_object_equals(response.values, expected, `JS encoding and C++ decoding of: ${method}`);
});
}
const structFields = [

@ -27,6 +27,16 @@ class ResponseParamsJsImpl {
'getNullFloat',
'getNullDouble',
'getNullEnum',
'getNullBools',
'getNullInt16s',
'getNullUint32s',
'getNullDoubles',
'getNullEnums',
'getNullBoolMap',
'getNullInt32Map',
'getNullEnumMap',
];
for (const method of nullMethods) {
this[method] = this.getNullOptional;
@ -45,6 +55,16 @@ class ResponseParamsJsImpl {
'getOptionalFloat',
'getOptionalDouble',
'getOptionalEnum',
'getOptionalBools',
'getOptionalInt16s',
'getOptionalUint32s',
'getOptionalDoubles',
'getOptionalEnums',
'getOptionalBoolMap',
'getOptionalFloatMap',
'getOptionalEnumMap',
];
for (const method of methods) {
this[method] = this.getOptional;
@ -279,6 +299,35 @@ promise_test(async () => {
}
}, 'JS decoding of null struct with optional numerics.');
const testGetArraysOfNullsMethods = [
'getNullBools',
'getNullInt16s',
'getNullUint32s',
'getNullDoubles',
'getNullEnums',
];
for (const method of testGetArraysOfNullsMethods) {
promise_test(async() => {
const response = await cpp[method]();
assert_array_equals(response.optionalValues, [null]);
});
}
const testGetMapOfNullsMethods = [
'getNullBoolMap',
'getNullInt32Map',
'getNullEnumMap',
];
for (const method of testGetMapOfNullsMethods) {
promise_test(async() => {
const response = await cpp[method]();
assert_object_equals(response.optionalValues, {0: null});
});
}
const testMethods = [{
method: 'getOptionalBool',
valueToUse: true,
@ -459,3 +508,52 @@ promise_test(async () => {
assert_equals(s[field.name], field.value, field.name);
}
}, 'JS encoding of struct with optional numerics in response params.');
const testNullWrapping = [{
name: 'getOptionalBools',
value: true,
}, {
name: 'getOptionalInt16s',
value: 16,
}, {
name: 'getOptionalUint32s',
value: 32,
}, {
name: 'getOptionalDoubles',
value: 22.2,
}, {
name: 'getOptionalEnums',
value: OptionalNumericsRegularEnum.kFoo,
}];
for (const {name, value} of testNullWrapping) {
promise_test(async() => {
const response = await cpp[name](value);
assert_array_equals(response.optionalValues, [null, value, null]);
});
}
const testNullWrappingForMap = [{
name: 'getOptionalBoolMap',
key: 6,
value: false,
}, {
name: 'getOptionalFloatMap',
key: 7,
value: 1.25,
}, {
name: 'getOptionalEnumMap',
key: 8,
value: OptionalNumericsRegularEnum.kFoo,
}];
for (const {name, key, value} of testNullWrappingForMap) {
promise_test(async() => {
const response = await cpp[name](key, value);
assert_object_equals(response.optionalValues, {
[key - 1]: null,
[key]: value,
[key + 1]: null,
});
});
}