Description
Background
dart:ffi
has a number of APIs which require arguments or type arguments to be compile time constants to facilitate static analysis and tree-shaking. For example, funcPtr.asFunction<DF>()
requires DF
to be a compile time constant, so that compiler could generate appropriate specialised trampoline which handles Dart->C calls. Similar restriction (for the very same reason) applies to the dual API Pointer.fromFunction(dartFunc)
, where dartFunc
is expected a compile-time constant (static function tear-off).
Currently these restrictions are enforced using custom dart:ffi
specific CFE/analyzer passes. These passes have two problems:
- They are
dart:ffi
specific and can't be reused when a similar need arises in some other API. One example of an API which could benefit from this isPluginUtilities.getCallbackHandle
indart:ui
(see also PluginUtilities.getCallbackHandle and tree-shaking flutter/flutter#118608) - They prevent users from hiding restricted APIs inside their code as an implementation detail:
void doSomethingAndCall(void Function() callback) { c_library.doSomethingAndCall(Pointer.fromFunction(callback)); // error: callback is not a constant } void doSomethingAndCall(Pointer<...> callback) { // FFI type leaks into API c_library.doSomethingAndCall(callback); }
We have previously experimented with building a feature like this in the linter: see internal @mustBeConst
proposal.
Proposal
Allow const
modifier on parameter and type parameter declarations:
class X {
const X();
}
class A<const T> {
void foo<const U>(const U u);
void bar(const X x, {const int v});
}
Adding const
to a parameter or type parameter declaration introduces the following restrictions:
- At a call site any parameter which has
const
on it must have either a constant argument value or an argument which refers to anotherconst
parameter. Other arguments are invalid.void Function(const X x) bar; void Function(const int y) baz; void foo(const X x, const int y) { bar(const X()); // ok bar(x); // ok bar(new X()); // error int v = 10; baz(1); // ok baz(y); // ok baz(v); // error }
- Similarly, at a call site any type parameter which has
const
on it must have either a constant fully instantiated type argument value or refer to another const type parameter. Other type arguments are invalid.void Function<const T>() bar; void foo<const T>() { bar<int>(); // ok bar<T>(); // ok bar<List<T>>(); // error }
- Similar restrictions apply to type literals, e.g.
A<int>
is a valid instantiation ofA<const T>
and so isA<T>
ifT
is a const-type-parameter, butA<List<T>>
is incorrect. - Rules for subtyping (and consequenty for valid overrides) respect
const
on parameters and type parameters in an obvious way: given function typesA = R Function(const B)
andB = R Function(B)
B
is a subtype ofA
, but not the other way around. Consequently overriding a function might remove requirement for parameter to be constant, but can't add it. dynamic
calls to function which containsconst
parameters orconst
type parameters will result in an exception.
/cc @lrhn @leafpetersen @eernstg @munificent @dcharkes @mkustermann