Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Breaking Change Request] [vm/ffi] Disallow invoking ffi methods with generics #44621

Closed
dcharkes opened this issue Jan 8, 2021 · 12 comments
Closed
Assignees
Labels
area-vm Use area-vm for VM related issues, including code coverage, FFI, and the AOT and JIT backends. breaking-change-request This tracks requests for feedback on breaking changes library-ffi P1 A high priority bug; for example, a single project is unusable or has many test failures

Comments

@dcharkes
Copy link
Contributor

dcharkes commented Jan 8, 2021

Update 2021-01-22: We will deprecate invoking the methods with generics in Dart 2.12 and remove support in 2.13.

Summary

Disallow invoking the following methods in dart:ffi with a generic and with Pointer with a generic type argument.

sdk/sdk/lib/ffi/ffi.dart

Lines 24 to 27 in 897b1a2

/// Number of bytes used by native type T.
///
/// Includes padding and alignment of structs.
external int sizeOf<T extends NativeType>();

sdk/sdk/lib/ffi/ffi.dart

Lines 63 to 64 in 897b1a2

/// Pointer arithmetic (takes element size into account).
external Pointer<T> elementAt(int index);

sdk/sdk/lib/ffi/ffi.dart

Lines 535 to 547 in 897b1a2

/// Creates a reference to access the fields of this struct backed by native
/// memory at [address].
///
/// The [address] must be aligned according to the struct alignment rules of
/// the platform.
external T get ref;
/// Creates a reference to access the fields of this struct backed by native
/// memory at `address + sizeOf<T>() * index`.
///
/// The [address] must be aligned according to the struct alignment rules of
/// the platform.
external T operator [](int index);

And introduce the following to reduce boilerplate:

/// Manages memory on the native heap.
abstract class Allocator {
  /// This interface is meant to be implemented, not extended or mixed in.
  Allocator._() {
    throw UnsupportedError("Cannot be instantiated");
  }

  /// Allocates [byteCount] bytes of memory on the native heap.
  ///
  /// If [alignment] is provided, the allocated memory will be at least aligned
  /// to [alignment] bytes.
  ///
  /// Throws an [ArgumentError] if the number of bytes or alignment cannot be
  /// satisfied.
  Pointer<T> allocate<T extends NativeType>(int byteCount, {int? alignment});

  /// Releases memory allocated on the native heap.
  ///
  /// Throws an [ArgumentError] if the memory pointed to by [pointer] cannot be
  /// freed.
  void free(Pointer pointer);
}

/// Extension on [Allocator] to provide allocation with [NativeType].
extension AllocatorAlloc on Allocator {
  /// Allocates `sizeOf<T>() * count` bytes of memory using
  /// `this.allocate`.
  ///
  /// This extension method must be invoked with a compile-time constant [T].
  external Pointer<T> call<T extends NativeType>([int count = 1]);
}

Motivation

This will enable us to do multiple things.

  1. Tree shaking of user-defined subclasses of Struct ([vm/ffi] Support treeshaking of FFI structs #38721).
  2. Optimize invocations of this code by directly inlining the size ([vm/ffi] Optimize Pointer<Struct> operations #38648).
  3. Standardize user-defined allocators: arena style allocation (sample), bump pointer allocation ([vm/ffi] Support treeshaking of FFI structs #38721 (comment)), and wrappers to track allocation.

Impact

package:ffi's allocate<T extends NativeType>() will stop working.

User code using sizeOf, .ref, and elementAt generically will stop working. However, at this moment we are not aware of any code written in such way. This breaking change request is to assess whether code such as that is out there.

This requires changes API to take an int argument for the size or offset and let their callees invoke sizeOf with a non-generic type argument.

// Before breaking change.
final pointer = allocate<Int8>(count: 10);

// After breaking change (without boilerplate reduction).
final pointer = allocate<Int8>(sizeOf<Int8>() * 10);

Boilerplate reduction for allocation

Repeating the type argument is inconvenient. To address this we introduce the Allocator interface and call extension method on Allocator to dart:ffi (see summary).

// Before breaking change.
final pointer = allocate<Int8>(count: 10);

// After breaking change.
final pointer = allocator<Int8>(10);

Proposed changes: https://dart-review.googlesource.com/c/sdk/+/177705/10/sdk/lib/ffi/allocation.dart

Migration of package:ffi

We remove allocate from package:ffi.

We introduce CallocAllocator and CallocAllocator which implement the Allocator interface and consts calloc and calloc.

class CallocAllocator implements Allocator {
  Pointer<T> allocate<T extends NativeType>(int numBytes);

  void free(Pointer pointer);
}

const calloc = CallocAllocator();

Proposed changes: dart-archive/ffi@b072465#diff-33eeafb9384f3239bb7f203cab73b7d099a5caa20a2033ef0a28b8019a57d647

Migration of users of package:ffi

// Before breaking change.
final pointer = allocate<Int8>(count: 10);
free(pointer);

// After breaking change.
final pointer = calloc<Int8>(10);
calloc.free(pointer);

Example migrations:

Discussion

Zero-initialize memory

We have two options for initializing memory to zero.

  1. Add calloc besides malloc to package:ffi. This would use the calloc on all non-Windows OSes, and HEAP_ZERO_MEMORY flag for HeapAlloc on Windows. (This is the option that is prototyped.)
  2. Add a named argument bool zeroInitialize = false to allocate and Allocator.allocate.

In the first one, the implementor of the Allocator has control over whether something is zero-initialized. In the latter one, the caller of allocate has control over whether something is zero-initialized.

Requires-constant-type-arguments

The need to disallow .ref on Pointer<T extends Struct> stems from the fact that Dart does not support calling new T();

We could explore adding a pragma that would inline function bodies, or duplicate them with instantiated type arguments, as a more general solution to this problem.

@pragma('requires-constant-type-arguments')
T instantiateSomethingGeneric<T extends Object>(){
  // The following would be allowed because of the pragma.
  return new T();
}

class MyClass {}

void main(){
  final myClass = instantiateSomethingGeneric<MyClass>();
  print(myClass);
}

When exploring this we should check what this kind of logic would to to code size.

Thanks to @mraleph for this suggestion.

@dcharkes dcharkes added area-vm Use area-vm for VM related issues, including code coverage, FFI, and the AOT and JIT backends. breaking-change-request This tracks requests for feedback on breaking changes library-ffi labels Jan 8, 2021
@dcharkes
Copy link
Contributor Author

dcharkes commented Jan 11, 2021

https://dart-review.googlesource.com/c/sdk/+/177705/8/sdk/lib/ffi/allocation.dart#24

@lrhn suggested using an extension method instead of a top level method for allocate:

// Before breaking change.
final pointer = allocate<Int8>(count: 10);
free(pointer);

// After breaking change with extension method `alloc` (`allocate` conflicts with instance method).
final pointer = malloc.alloc<Int8>(10);
malloc.free(pointer);

// After breaking change with extension method `call`.
final pointer = malloc<Int8>(10);
malloc.free(pointer);

The call option would result in the following with arena allocation and bump allocation (#38721 (comment)):

final pointer = pool<Int8>(10);
pool.freeAll();

final pointer = bumpAllocator<Int8>(10);
bumpAllocator.free(pointer);

We have 3 options:

  1. Allocator.allocate instance method + Malloc.call extension method (prototyped in linked CLs).
  2. Allocator.allocate instance method + Malloc.alloc extension method.
  3. Allocator.allocateBytes instance method + Malloc.allocate extension method.

Any preferences?

@franklinyow
Copy link
Contributor

@dcharkes Please make an announcement to https://groups.google.com/a/dartlang.org/g/announce

cc @Hixie @matanlurey @vsmenon for review and approval.

@Hixie
Copy link
Contributor

Hixie commented Jan 11, 2021

I have no opinion on this and thus no objection. I would encourage this change to be amply documented, in particular with the error message someone would see included in the documentation so that if they google for it they will find it.

@vsmenon
Copy link
Member

vsmenon commented Jan 12, 2021

lgtm

fyi - @leafpetersen - if we expect more general use for compile-time known type params.

@matanlurey
Copy link
Contributor

I don't work in this area, no comment.

@dcharkes
Copy link
Contributor Author

@franklinyow
Copy link
Contributor

Approved

dart-bot pushed a commit that referenced this issue Jan 13, 2021
Introduces the Allocator API in `dart:ffi`.

This CL does not yet roll `package:ffi` to use `Allocator`, because that
breaks the checked in Dart in Fluter in g3. Instead, this coppies
`_MallocAllocator` from `package:ffi` into the ffi tests for testing.

This CL does not yet migrate off `allocate` and `free` in the SDK. That
is done in a dependent CL.

Issue: #44621
Issue: #38721

TEST=tests/ffi/allocator_test.dart
TEST=tests/ffi/calloc_test.dart
TEST=tests/ffi/vmspecific_static_checks_test.dart

Change-Id: I173e213a750b8b3f594bb8d4fc72575f2b6b91f7
Cq-Include-Trybots: luci.dart.try:vm-precomp-ffi-qemu-linux-release-arm-try,analyzer-analysis-server-linux-try,analyzer-linux-release-try,analyzer-nnbd-linux-release-try,front-end-linux-release-x64-try,front-end-nnbd-linux-release-x64-try,benchmark-linux-try,dart-sdk-linux-try,pkg-linux-release-try,vm-ffi-android-release-arm-try,vm-ffi-android-release-arm64-try,vm-kernel-nnbd-win-debug-x64-try,vm-kernel-win-debug-x64-try
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/177705
Reviewed-by: Clement Skau <cskau@google.com>
dart-bot pushed a commit that referenced this issue Jan 14, 2021
New API landed in: https://dart-review.googlesource.com/c/sdk/+/177705

Issue: #44621
Issue: #38721

Change-Id: Id45274313edbb3842438b66b9c0917a86884c8ed
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/178993
Reviewed-by: Aske Simon Christensen <askesc@google.com>
Commit-Queue: Daco Harkes <dacoharkes@google.com>
dart-bot pushed a commit that referenced this issue Jan 14, 2021
This CL does not yet roll `package:ffi` to use `Allocator`, because that
breaks the checked in Dart in Flutter in g3. Instead, this uses the copy
of `_CallocAllocator` from `package:ffi` in `calloc.dart` in tests/ffi.

New API landed in: https://dart-review.googlesource.com/c/sdk/+/177705

Issue: #44621
Issue: #38721

Change-Id: Iedfc4a11d4606915a324c824372bca643016f5a3
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/178994
Commit-Queue: Daco Harkes <dacoharkes@google.com>
Reviewed-by: Aske Simon Christensen <askesc@google.com>
dart-bot pushed a commit that referenced this issue Jan 14, 2021
This CL does not yet roll `package:ffi` to use `Allocator`, because that
breaks the checked in Dart in Flutter in g3. Instead, this copies
`_CallocAllocator` from `package:ffi` into the benchmarks. (We need a
copy per benchmark such that file-copying before running benchmarks
works properly.) The copies can be deleted when we can update
`package:ffi` in the DEPS file to contain `_CallocAllocator`.

New API landed in: https://dart-review.googlesource.com/c/sdk/+/177705

Issue: #44621
Issue: #38721

Change-Id: I546de7ec65ceb6f05644a5f269b83f64656892e5
Cq-Include-Trybots: luci.dart.try:benchmark-linux-try
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/178995
Commit-Queue: Daco Harkes <dacoharkes@google.com>
Reviewed-by: Aske Simon Christensen <askesc@google.com>
dart-bot pushed a commit that referenced this issue Jan 14, 2021
This CL does not yet roll `package:ffi` to use `Allocator`, because that
breaks the checked in Dart in Flutter in g3. Instead, this uses the copy
of `_CallocAllocator` from `package:ffi` in `calloc.dart`.

New API landed in: https://dart-review.googlesource.com/c/sdk/+/177705

Issue: #44621
Issue: #38721

Change-Id: I10e53a363cd87c4c11e7042989e6957b82ae2a09
Cq-Include-Trybots: luci.dart.try:vm-kernel-win-debug-x64-try,vm-kernel-linux-debug-x64-try,vm-kernel-mac-debug-x64-try
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/178992
Reviewed-by: Aske Simon Christensen <askesc@google.com>
Commit-Queue: Daco Harkes <dacoharkes@google.com>
dart-bot pushed a commit that referenced this issue Jan 14, 2021
This CL does not yet roll `package:ffi` to use `Allocator`, because that
breaks the checked in Dart in Flutter in g3. Instead, this copies
`_CallocAllocator` from `package:ffi` into the samples.

New API landed in: https://dart-review.googlesource.com/c/sdk/+/177705

Issue: #44621
Issue: #38721

Change-Id: I83da349c2e52d7f079aa1569b4726318fee24c9d
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/177706
Commit-Queue: Daco Harkes <dacoharkes@google.com>
Reviewed-by: Aske Simon Christensen <askesc@google.com>
dart-bot pushed a commit that referenced this issue Jan 20, 2021
This can only be landed when `Allocator` and `Opaque` have rolled into
Flutter/engine, that into Flutter/flutter, and that into g3.
flutter/flutter/commit/a706cd211240f27be3b61f06d70f958c7a4156fe

Deletes all the copies of `_CallocAllocator` and uses the one from
`package:ffi` instead.

Issue: #44622
Issue: #43974
Issue: #44621
Issue: #38721

Change-Id: I50b3b4c31a2b839b35e3e057bd54f463b90bc55e
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/179540
Reviewed-by: Aske Simon Christensen <askesc@google.com>
dart-bot pushed a commit that referenced this issue Jan 22, 2021
Issue: #44621
Issue: #44622

Change-Id: I9689db63a5b47946b501fac9df63cc86811d6e41
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/179764
Reviewed-by: Vyacheslav Egorov <vegorov@google.com>
@dcharkes dcharkes added the P1 A high priority bug; for example, a single project is unusable or has many test failures label Jan 22, 2021
@dcharkes dcharkes added this to the January Beta Release (2.12) milestone Jan 22, 2021
@dcharkes
Copy link
Contributor Author

We have landed deprecation hints for invoking the methods with d74b2f4 and f74c9d4.

Turning these hints into errors and making use of the static types to do tree shaking (#38721) and optimizations (#38648) is tracked in the respective issues. Closing this issue.

dcharkes added a commit to dart-archive/ffi that referenced this issue Feb 1, 2021
Changes `Utf8` and `Utf16` to extend `Opaque` instead of `Struct`.
This means `.ref` is no longer available and `Pointer<Utf(..)>` should be used.
See [breaking change #44622](dart-lang/sdk#44622) for more info.

Removes `allocate` and `free`.
Instead, introduces `calloc` which implements the new `Allocator` interface.
See [breaking change #44621](dart-lang/sdk#44621) for more info.

This pre-release requires Dart `2.12.0-265.0.dev` or greater.
dart-bot pushed a commit that referenced this issue Feb 2, 2021
This rewrites `sizeOf` calls in the CFE to skip the runtime entry when
the type argument is constant.

The runtime entry is still used when the type argument is generic.
Forcing the type argument to be constant and removing the runtime entry
will be done in follow up CLs.

Bug: #44621
Bug: #38721

TEST=tests/ffi/sizeof_test.dart

Change-Id: I17d14432e6ab22810729be6b5c2939a033d382c5
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/182262
Reviewed-by: Clement Skau <cskau@google.com>
dart-bot pushed a commit that referenced this issue Feb 2, 2021
This rewrites `elementAt` calls in the CFE to skip the runtime entry
for `sizeOf` when the type argument is constant.

The runtime entry is still used when the type argument is generic.
Forcing the type argument to be constant and removing the runtime entry
will be done in follow up CLs.

Bug: #44621
Bug: #38721

TEST=tests/ffi/data_test.dart

Change-Id: I480db43e7c115c24bd45f0ddab0cfea7eb8cfa58
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/182263
Reviewed-by: Clement Skau <cskau@google.com>
dart-bot pushed a commit that referenced this issue Feb 2, 2021
This rewrites `Allocator.call` calls in the CFE to skip the runtime
entry for `sizeOf` when the type argument is constant.

The runtime entry is still used when the type argument is generic.
Forcing the type argument to be constant and removing the runtime entry
will be done in follow up CLs.

Bug: #44621
Bug: #38721

TEST=test/ffi (almost all of them)

Change-Id: I5e855fa2b63a5c1b7fa70dbaa1b89c122a82da6e
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/182264
Reviewed-by: Clement Skau <cskau@google.com>
dart-bot pushed a commit that referenced this issue Feb 2, 2021
This rewrites `Pointer<Struct>.ref` and `Pointer<Struct>[]` calls in
the CFE to skip the runtime entry when the type argument is constant.

The runtime entry is still used when the type argument is generic.
Forcing the type argument to be constant and removing the runtime entry
will be done in follow up CLs.

Bug: #38721
Bug: #44621

Removing the runtime entry speeds up `.ref` significantly.
before: FfiStruct.FieldLoadStore(RunTime): 18868.140186915887 us.
after: FfiStruct.FieldLoadStore(RunTime): 270.5877976190476 us.
Measurements from Linux x64 in JIT mode.

Closes: #38648

TEST=tests/ffi/structs_test.dart

Change-Id: I82abd930b5a9c5c78a8999c2bc49802d67d37534
Cq-Include-Trybots: luci.dart.try:analyzer-linux-release-try,analyzer-nnbd-linux-release-try,app-kernel-linux-debug-x64-try,dart-sdk-linux-try,front-end-nnbd-linux-release-x64-try,pkg-linux-debug-try,vm-kernel-linux-debug-x64-try,vm-kernel-nnbd-linux-debug-x64-try,vm-kernel-precomp-linux-debug-x64-try,vm-kernel-precomp-nnbd-linux-debug-x64-try,vm-kernel-reload-linux-debug-x64-try,vm-kernel-reload-rollback-linux-debug-x64-try
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/182265
Commit-Queue: Daco Harkes <dacoharkes@google.com>
Reviewed-by: Clement Skau <cskau@google.com>
dart-bot pushed a commit that referenced this issue Feb 10, 2021
This can only be landed when `Allocator`, `Opaque`, and `AllocatorAlloc`
have rolled into Flutter/engine, that into Flutter/flutter, and into g3.

Deletes all the copies of `_CallocAllocator` and uses the one from
`package:ffi` instead.

Bug: #44622
Bug: #43974
Bug: #44621
Bug: #38721

Change-Id: I486034b379b5a63cad4aefd503ccd0f2ce5dd45e
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/180188
Commit-Queue: Daco Harkes <dacoharkes@google.com>
Reviewed-by: Aske Simon Christensen <askesc@google.com>
@pezi
Copy link

pezi commented Feb 18, 2021

I am honest - I am Dart nebiew and for my first little hobby project
https://pub.dev/packages/dart_periphery
I use ffi to maniplate low level structs - e.g. i2c_msg for the I2C bus.

Currently I build complete native structs on the Dart side. It will possible to migrate this code according the new ffi changes?
Sorry for my missing knowlege to understand the upcoming changes in their full range.

/// Helper class mapped to the C struct `
class NativeI2Cmsg extends Struct {
  @Int16()
  int addr;
  @Int16()
  int flags;
  @Int16()
  int len;
  Pointer<Int8> buf;
  factory NativeI2Cmsg.allocate() => allocate<NativeI2Cmsg>().ref;
}
static Pointer<NativeI2Cmsg> _toNative(List<I2Cmsg> list) {
    final ptr = allocate<NativeI2Cmsg>(count: list.length);
    var index = 0;
    for (var data in list) {
      var msg = ptr.elementAt(index++);
      msg.ref.addr = data.addr;
      msg.ref.len = data.len;
      var flags = 0;
      if (data.flags.isNotEmpty) {
        for (var f in data.flags) {
          flags |= I2CmsgFlags2Int(f);
        }
      }
      msg.ref.flags = flags;
      msg.ref.buf = allocate<Int8>(count: data.len);
      if (data.predefined.isNotEmpty) {
        var count = 0;
        for (var value in data.predefined) {
          msg.ref.buf.elementAt(count++).value = value;
        }
      }
    }
    return ptr;
  }

@dcharkes
Copy link
Contributor Author

dcharkes commented Feb 18, 2021

Yes, just fire up the IDE with the latest beta release to get guidance from the analyzer hints.

/// Helper class mapped to the C struct `
class NativeI2Cmsg extends Struct {
  @Int16()
  external int addr;
  @Int16()
  external int flags;
  @Int16()
  external int len;
  external Pointer<Int8> buf;
}

Pointer<NativeI2Cmsg> _toNative(List<I2Cmsg> list) {
  final ptr = calloc<NativeI2Cmsg>(list.length);
  var index = 0;
  for (var data in list) {
    final msg = ptr.elementAt(index++);
    msg.ref.addr = data.addr;
    msg.ref.len = data.len;
    var flags = 0;
    if (data.flags.isNotEmpty) {
      for (var f in data.flags) {
        flags |= I2CmsgFlags2Int(f);
      }
    }
    msg.ref.flags = flags;
    msg.ref.buf = calloc<Int8>(data.len);
    if (data.predefined.isNotEmpty) {
      var count = 0;
      for (var value in data.predefined) {
        msg.ref.buf.elementAt(count++).value = value;
      }
    }
  }
  return ptr;
}

@maks
Copy link

maks commented Oct 18, 2021

Sorry @dcharkes its very late in the game to ask about this, but just to double check, now that this change has been made, allocating for generics as described in: https://stackoverflow.com/questions/68018694/type-safe-memory-allocation-with-dart-ffi is now not possible?

@dcharkes
Copy link
Contributor Author

@maks We already support that use case now with providing your own Allocator class. Your specific use case is already supported with the Arena allocator from package:ffi. https://stackoverflow.com/questions/68018694/type-safe-memory-allocation-with-dart-ffi/69611982#69611982

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-vm Use area-vm for VM related issues, including code coverage, FFI, and the AOT and JIT backends. breaking-change-request This tracks requests for feedback on breaking changes library-ffi P1 A high priority bug; for example, a single project is unusable or has many test failures
Projects
None yet
Development

No branches or pull requests

7 participants