Skip to content

Commit

Permalink
[vm/ffi] Support inline arrays in Structs backend
Browse files Browse the repository at this point in the history
The various ABIs lay out structs with inline arrays the same way as
they do with only nested structs and primitive values.
Most notably, homogenous structs (arm and arm64) and structs spread
over a CPU and FPU register in x64 Linux/MacOS will even be laid out
this way if irregular size nested arrays are involved.
These cases are covered in the unit tests.

This CL introduces the ByteRange to ease the ContainsOnlyFloats
calculation for x64.

Bug: #35763

tools/build.py run_ffi_unit_tests && tools/test.py ffi_unit
TEST=runtime/vm/compiler/ffi/native_calling_convention_test.cc
TEST=runtime/vm/compiler/ffi/native_type_test.cc

tools/test.py ffi ffi_2
TEST=tests/ffi(_2)/(.*)by_value_(*.)_test.dart

Change-Id: I4bbcbffd47eb8901a87db64e62aa5cbe67d03e18
Cq-Include-Trybots: luci.dart.try:vm-precomp-ffi-qemu-linux-release-arm-try,vm-kernel-win-debug-ia32-try,vm-kernel-win-debug-x64-try,vm-kernel-precomp-nnbd-linux-debug-x64-try,vm-kernel-linux-debug-ia32-try,vm-kernel-mac-debug-x64-try,vm-ffi-android-debug-arm64-try,vm-ffi-android-debug-arm-try
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/183682
Commit-Queue: Daco Harkes <dacoharkes@google.com>
Reviewed-by: Clement Skau <cskau@google.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
  • Loading branch information
dcharkes authored and commit-bot@chromium.org committed Feb 10, 2021
1 parent 88c617d commit 7a4d1c1
Show file tree
Hide file tree
Showing 38 changed files with 567 additions and 46 deletions.
10 changes: 5 additions & 5 deletions runtime/vm/compiler/ffi/native_calling_convention.cc
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,8 @@ class ArgumentAllocator : public ValueObject {
zone_, required_regs + required_xmm_regs);
for (intptr_t offset = 0; offset < size;
offset += compiler::target::kWordSize) {
if (payload_type.ContainsOnlyFloats(
offset, Utils::Minimum<intptr_t>(size - offset, 8))) {
if (payload_type.ContainsOnlyFloats(Range::StartAndEnd(
offset, Utils::Minimum<intptr_t>(size, offset + 8)))) {
const intptr_t reg_index = FirstFreeFpuRegisterIndex(kQuadFpuReg);
AllocateFpuRegisterAtIndex(kQuadFpuReg, reg_index);
const auto& type = *new (zone_) NativePrimitiveType(kDouble);
Expand Down Expand Up @@ -527,8 +527,8 @@ static const NativeLocation& CompoundResultLocation(
const auto& double_type = *new (zone) NativePrimitiveType(kDouble);
const auto& int64_type = *new (zone) NativePrimitiveType(kInt64);

const bool first_half_in_xmm =
payload_type.ContainsOnlyFloats(0, Utils::Minimum<intptr_t>(size, 8));
const bool first_half_in_xmm = payload_type.ContainsOnlyFloats(
Range::StartAndEnd(0, Utils::Minimum<intptr_t>(size, 8)));
if (first_half_in_xmm) {
multiple_locations.Add(new (zone) NativeFpuRegistersLocation(
double_type, double_type, kQuadFpuReg,
Expand All @@ -541,7 +541,7 @@ static const NativeLocation& CompoundResultLocation(
}
if (size > 8) {
const bool second_half_in_xmm = payload_type.ContainsOnlyFloats(
8, Utils::Minimum<intptr_t>(size - 8, 8));
Range::StartAndEnd(8, Utils::Minimum<intptr_t>(size, 16)));
if (second_half_in_xmm) {
const FpuRegister reg = used_xmm_regs == 0
? CallingConventions::kReturnFpuReg
Expand Down
103 changes: 103 additions & 0 deletions runtime/vm/compiler/ffi/native_calling_convention_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,50 @@ UNIT_TEST_CASE_WITH_ZONE(NativeCallingConvention_struct16bytesHomogenousx10) {
RunSignatureTest(Z, "struct16bytesHomogenousx10", arguments, struct_type);
}

// Test with homogenous struct (2).
//
// This time with nested structs and inline arrays.
//
// See the *.expect in ./unit_tests for this behavior.
UNIT_TEST_CASE_WITH_ZONE(NativeCallingConvention_struct16bytesHomogenousx10_2) {
const auto& float_type = *new (Z) NativePrimitiveType(kFloat);
const auto& int8type = *new (Z) NativePrimitiveType(kInt8);

const auto& float_1_array_type = *new (Z) NativeArrayType(float_type, 1);

const auto& float_2_array_type = *new (Z) NativeArrayType(float_type, 2);
auto& full_float_member_types = *new (Z) NativeTypes(Z, 1);
full_float_member_types.Add(&float_2_array_type);
const auto& float_array_struct_type =
NativeCompoundType::FromNativeTypes(Z, full_float_member_types);

auto& member_types = *new (Z) NativeTypes(Z, 3);
member_types.Add(&float_1_array_type);
member_types.Add(&float_array_struct_type);
member_types.Add(&float_type);
const auto& struct_type =
NativeCompoundType::FromNativeTypes(Z, member_types);

auto& arguments = *new (Z) NativeTypes(Z, 13);
arguments.Add(&struct_type);
arguments.Add(&float_type); // Claim a single FPU register.
arguments.Add(&struct_type);
arguments.Add(&struct_type);
arguments.Add(&struct_type);
arguments.Add(&struct_type);
arguments.Add(&struct_type);
arguments.Add(&struct_type);
arguments.Add(&struct_type);
arguments.Add(&struct_type);
arguments.Add(&float_type); // Check float register back filling, if any.
arguments.Add(&int8type); // Check integer register back filling, if any.
arguments.Add(&struct_type); // Check stack alignment of struct.

// Identical expectation files as previous test, struct contains the same
// members, but nested in arrays and nested structs.
RunSignatureTest(Z, "struct16bytesHomogenousx10", arguments, struct_type);
}

// A fairly big struct.
//
// On arm, split up in 8-byte chunks. The first chunk goes into two registers,
Expand Down Expand Up @@ -269,6 +313,65 @@ UNIT_TEST_CASE_WITH_ZONE(NativeCallingConvention_struct16bytesMixedx10_2) {

RunSignatureTest(Z, "struct16bytesMixedx10_2", arguments, struct_type);
}

// On x64 non-Windows a struct can be spread over an FPU and int register.
//
// This behavior also happens with nested structs and inline arrays.
//
// typedef struct {
// int32_t a0;
// float a1;
// } HalfFloat;
//
// typedef struct {
// float a1[1];
// } FullFloat;
//
// typedef struct {
// int32_t a0;
// HalfFloat a1;
// FullFloat a2;
// } HalfFloat2;
//
// See the *.expect in ./unit_tests for this behavior.
UNIT_TEST_CASE_WITH_ZONE(NativeCallingConvention_struct16bytesMixedx10_3) {
const auto& float_type = *new (Z) NativePrimitiveType(kFloat);
const auto& int32_type = *new (Z) NativePrimitiveType(kInt32);

auto& half_float_member_types = *new (Z) NativeTypes(Z, 2);
half_float_member_types.Add(&int32_type);
half_float_member_types.Add(&float_type);
const auto& half_float_type =
NativeCompoundType::FromNativeTypes(Z, half_float_member_types);

const auto& float_array_type = *new (Z) NativeArrayType(float_type, 1);
auto& full_float_member_types = *new (Z) NativeTypes(Z, 1);
full_float_member_types.Add(&float_array_type);
const auto& full_float_type =
NativeCompoundType::FromNativeTypes(Z, full_float_member_types);

auto& member_types = *new (Z) NativeTypes(Z, 3);
member_types.Add(&int32_type);
member_types.Add(&half_float_type);
member_types.Add(&full_float_type);
const auto& struct_type =
NativeCompoundType::FromNativeTypes(Z, member_types);

auto& arguments = *new (Z) NativeTypes(Z, 11);
arguments.Add(&struct_type);
arguments.Add(&struct_type);
arguments.Add(&struct_type);
arguments.Add(&struct_type);
arguments.Add(&struct_type);
arguments.Add(&struct_type);
arguments.Add(&struct_type); // Integer registers exhausted, on stack.
arguments.Add(&struct_type);
arguments.Add(&struct_type);
arguments.Add(&struct_type);
arguments.Add(&float_type); // Use remaining FPU register.

RunSignatureTest(Z, "struct16bytesMixedx10_3", arguments, struct_type);
}
#endif // defined(TARGET_ARCH_X64)

// On ia32 Windows a struct can be returned in registers, on non-Windows not.
Expand Down
138 changes: 110 additions & 28 deletions runtime/vm/compiler/ffi/native_type.cc
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ const NativePrimitiveType& NativeType::AsPrimitive() const {
return static_cast<const NativePrimitiveType&>(*this);
}

const NativeArrayType& NativeType::AsArray() const {
ASSERT(IsArray());
return static_cast<const NativeArrayType&>(*this);
}

const NativeCompoundType& NativeType::AsCompound() const {
ASSERT(IsCompound());
return static_cast<const NativeCompoundType&>(*this);
Expand Down Expand Up @@ -256,6 +261,14 @@ bool NativePrimitiveType::Equals(const NativeType& other) const {
return other.AsPrimitive().representation_ == representation_;
}

bool NativeArrayType::Equals(const NativeType& other) const {
if (!other.IsArray()) {
return false;
}
return other.AsArray().length_ == length_ &&
other.AsArray().element_type_.Equals(element_type_);
}

bool NativeCompoundType::Equals(const NativeType& other) const {
if (!other.IsCompound()) {
return false;
Expand Down Expand Up @@ -462,6 +475,16 @@ const char* NativeFunctionType::ToCString(Zone* zone) const {
return textBuffer.buffer();
}

void NativeArrayType::PrintTo(BaseTextBuffer* f,
bool multi_line,
bool verbose) const {
f->AddString("Array(");
f->Printf("element type: ");
element_type_.PrintTo(f, /*multi_line*/ false, verbose);
f->Printf(", length: %" Pd "", length_);
f->AddString(")");
}

void NativeCompoundType::PrintTo(BaseTextBuffer* f,
bool multi_line,
bool verbose) const {
Expand Down Expand Up @@ -518,6 +541,10 @@ intptr_t NativePrimitiveType::NumPrimitiveMembersRecursive() const {
return 1;
}

intptr_t NativeArrayType::NumPrimitiveMembersRecursive() const {
return element_type_.NumPrimitiveMembersRecursive() * length_;
}

intptr_t NativeCompoundType::NumPrimitiveMembersRecursive() const {
intptr_t count = 0;
for (intptr_t i = 0; i < members_.length(); i++) {
Expand All @@ -530,6 +557,10 @@ const NativePrimitiveType& NativePrimitiveType::FirstPrimitiveMember() const {
return *this;
}

const NativePrimitiveType& NativeArrayType::FirstPrimitiveMember() const {
return element_type_.FirstPrimitiveMember();
}

const NativePrimitiveType& NativeCompoundType::FirstPrimitiveMember() const {
ASSERT(NumPrimitiveMembersRecursive() >= 1);
for (intptr_t i = 0; i < members().length(); i++) {
Expand All @@ -540,30 +571,77 @@ const NativePrimitiveType& NativeCompoundType::FirstPrimitiveMember() const {
UNREACHABLE();
}

bool NativeCompoundType::ContainsOnlyFloats(intptr_t offset_in_bytes,
intptr_t size_in_bytes) const {
ASSERT(size_in_bytes >= 0);
const intptr_t first_byte = offset_in_bytes;
const intptr_t last_byte = offset_in_bytes + size_in_bytes - 1;
#if !defined(DART_PRECOMPILED_RUNTIME)
bool NativePrimitiveType::ContainsOnlyFloats(Range range) const {
const auto this_range = Range::StartAndEnd(0, SizeInBytes());
ASSERT(this_range.Contains(range));

return IsFloat();
}

bool NativeArrayType::ContainsOnlyFloats(Range range) const {
const auto this_range = Range::StartAndEnd(0, SizeInBytes());
ASSERT(this_range.Contains(range));

const intptr_t element_size_in_bytes = element_type_.SizeInBytes();

// Assess how many elements are (partially) covered by the range.
const intptr_t first_element_start = range.start() / element_size_in_bytes;
const intptr_t last_element_index =
range.end_inclusive() / element_size_in_bytes;
const intptr_t num_elements = last_element_index - first_element_start + 1;
ASSERT(num_elements >= 1);

if (num_elements > 2) {
// At least one full element covered.
return element_type_.ContainsOnlyFloats(
Range::StartAndLength(0, element_size_in_bytes));
}

// Check first element, which falls (partially) in range.
const intptr_t first_start = first_element_start * element_size_in_bytes;
const auto first_range =
Range::StartAndLength(first_start, element_size_in_bytes);
const auto first_range_clipped = range.Intersect(first_range);
const auto range_in_first = first_range_clipped.Translate(-first_start);
if (!element_type_.ContainsOnlyFloats(range_in_first)) {
// First element contains not only floats in specified range.
return false;
}

if (num_elements == 2) {
// Check the second (and last) element, which falls (partially) in range.
const intptr_t second_element_index = first_element_start + 1;
const intptr_t second_start = second_element_index * element_size_in_bytes;
const auto second_range =
Range::StartAndLength(second_start, element_size_in_bytes);
const auto second_range_clipped = range.Intersect(second_range);
const auto range_in_second = second_range_clipped.Translate(-second_start);
return element_type_.ContainsOnlyFloats(range_in_second);
}

return true;
}

bool NativeCompoundType::ContainsOnlyFloats(Range range) const {
const auto this_range = Range::StartAndEnd(0, SizeInBytes());
ASSERT(this_range.Contains(range));

for (intptr_t i = 0; i < members_.length(); i++) {
const intptr_t member_first_byte = member_offsets_[i];
const intptr_t member_last_byte =
member_first_byte + members_[i]->SizeInBytes() - 1;
if ((first_byte <= member_first_byte && member_first_byte <= last_byte) ||
(first_byte <= member_last_byte && member_last_byte <= last_byte)) {
if (members_[i]->IsPrimitive() && !members_[i]->IsFloat()) {
const auto& member = *members_[i];
const intptr_t member_offset = member_offsets_[i];
const intptr_t member_size = member.SizeInBytes();
const auto member_range = Range::StartAndLength(member_offset, member_size);
if (range.Overlaps(member_range)) {
const auto member_range_clipped = member_range.Intersect(range);
const auto range_in_member =
member_range_clipped.Translate(-member_offset);
if (!member.ContainsOnlyFloats(range_in_member)) {
// Member contains not only floats in specified range.
return false;
}
if (members_[i]->IsCompound()) {
const auto& nested = members_[i]->AsCompound();
const bool nested_only_floats = nested.ContainsOnlyFloats(
offset_in_bytes - member_first_byte, size_in_bytes);
if (!nested_only_floats) {
return false;
}
}
}
if (member_first_byte > last_byte) {
if (member_range.After(range)) {
// None of the remaining members fits the range.
break;
}
Expand All @@ -574,13 +652,14 @@ bool NativeCompoundType::ContainsOnlyFloats(intptr_t offset_in_bytes,
intptr_t NativeCompoundType::NumberOfWordSizeChunksOnlyFloat() const {
// O(n^2) implementation, but only invoked for small structs.
ASSERT(SizeInBytes() <= 16);
const auto this_range = Range::StartAndEnd(0, SizeInBytes());
const intptr_t size = SizeInBytes();
intptr_t float_only_chunks = 0;
for (intptr_t offset = 0; offset < size;
offset += compiler::target::kWordSize) {
if (ContainsOnlyFloats(
offset, Utils::Minimum<intptr_t>(size - offset,
compiler::target::kWordSize))) {
const auto chunk_range =
Range::StartAndLength(offset, compiler::target::kWordSize);
if (ContainsOnlyFloats(chunk_range.Intersect(this_range))) {
float_only_chunks++;
}
}
Expand All @@ -593,19 +672,22 @@ intptr_t NativeCompoundType::NumberOfWordSizeChunksNotOnlyFloat() const {
compiler::target::kWordSize;
return total_chunks - NumberOfWordSizeChunksOnlyFloat();
}
#endif // !defined(DART_PRECOMPILED_RUNTIME)

static void ContainsHomogenuousFloatsRecursive(const NativeTypes& types,
bool* only_float,
bool* only_double) {
for (intptr_t i = 0; i < types.length(); i++) {
const auto& member_type = types.At(i);
if (member_type->IsPrimitive()) {
PrimitiveType type = member_type->AsPrimitive().representation();
const auto& type = *types.At(i);
const auto& member_type =
type.IsArray() ? type.AsArray().element_type() : type;
if (member_type.IsPrimitive()) {
PrimitiveType type = member_type.AsPrimitive().representation();
*only_float = *only_float && (type == kFloat);
*only_double = *only_double && (type == kDouble);
}
if (member_type->IsCompound()) {
ContainsHomogenuousFloatsRecursive(member_type->AsCompound().members(),
if (member_type.IsCompound()) {
ContainsHomogenuousFloatsRecursive(member_type.AsCompound().members(),
only_float, only_double);
}
}
Expand Down
Loading

0 comments on commit 7a4d1c1

Please sign in to comment.