Files
sdk/runtime/docs/compiler/aot/entry_point_pragma.md
T
Tess Strickland 1ac77f57dd [vm] Align entry point verification and the precompiler.
For entry-point pragma annotations, most of the time they are
used with either no argument or with an argument that evaluates
to either

* false to denote the annotation should not take effect, or
* null or true to denote the annotation should take effect.

However, the user can also specify that only part of the operations
on a member should be accessed from native code by using a string
argument that is either 'call', 'set', or 'get'.

The entry point verification in Invoke/InvokeGetter/InvokeSetter
assumes that for getters and setters, the only valid string argument
is 'get' or 'set', respectively. This is because those methods are
called via `Dart_GetField`[0] and `Dart_SetField`, respectively, as if
they were the getter or setter of a defined field.

However, the precompiler previously assumed that the string
argument 'call' was the only string argument that meant the link
to a function's code object should be saved. Similarly, it assumed the
string argument 'get' for functions meant that their implicit closure
function should be saved, which ends up including getters. Furthermore,
it did not do anything with setters annotated with the string argument
'set'. This means that the code link would not be saved for getters or
setters that were annotated with the string argument expected by the
entry point verifier.

This CL aligns the precompiler to match the expectations of other
parts of the codebase. It also changes TFA to report an error
if a getter or setter is marked with the string argument 'call'.

[0] `Dart_Invoke` can be called with the name of a getter that
returns a closure, but doing so is semantically equivalent to
calling `Dart_GetField` followed by `Dart_InvokeClosure`.

TEST=vm/dart/entrypoint_verification_test

Fixes: https://github.com/dart-lang/sdk/issues/59920
Change-Id: Ia2768bbaf9058bb14a1cdfb331eb85fa082a0e90
Cq-Include-Trybots: luci.dart.try:vm-aot-dwarf-linux-product-x64-try,vm-aot-linux-debug-x64-try,vm-aot-linux-product-x64-try,vm-aot-mac-product-arm64-try,vm-aot-obfuscate-linux-release-x64-try,vm-linux-debug-x64-try,vm-linux-release-x64-try
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/404823
Reviewed-by: Alexander Markov <alexmarkov@google.com>
Commit-Queue: Tess Strickland <sstrickl@google.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
2025-01-24 03:21:22 -08:00

5.6 KiB

vm:entry-point pragma

The annotation @pragma("vm:entry-point", ...) must be placed on a class or member to indicate that it may be resolved, allocated or invoked directly from native or VM code in AOT mode.

To reduce the differences between JIT and AOT mode, entry point annotations are also checked in JIT mode except for uses of the dart:mirrors library and debugging uses via the VM service.

Background

Dart VM precompiler (AOT compiler) performs whole-program optimizations such as tree shaking and type flow analysis (TFA) in order to decrease size of the resulting compiled apps and improve their performance. Such optimizations assume that compiler can see the whole Dart program, and is able to discover and analyse all Dart functions and members which can be potentially executed at run time. While the Dart code is fully available for precompiler, native code of the embedder and native methods are out of reach of the compiler. Such native code can call back to Dart via native Dart API.

In order to guide precompiler, programmer must explicitly list entry points (roots) - Dart classes and members which are accessed from native code. Note that listing entry points is not optional: as long as program defines native methods which call into Dart, the entry points are required for the correctness of compilation.

In addition, when obfuscation is enabled, the precompiler needs to know which symbols need to be preserved to ensure they can be resolved from native code.

Syntax

The allowed uses of the annotation are as follows.

Classes

Any one of the following forms may be attached to a class:

@pragma("vm:entry-point")
@pragma("vm:entry-point", true/false)
@pragma("vm:entry-point", !const bool.fromEnvironment("dart.vm.product"))
class C { ... }

If the second parameter is missing, null or true, the class will be available for allocation directly from native or VM code.

Note that @pragma("vm:entry-point") may be added to abstract classes -- in this case, their name will survive obfuscation, but they won't have any allocation stubs.

Getters

Any one of the following forms may be attached to getters:

@pragma("vm:entry-point")
@pragma("vm:entry-point", true/false)
@pragma("vm:entry-point", !const bool.fromEnvironment("dart.vm.product"))
@pragma("vm:entry-point", "get")
void get foo { ... }

The "get" annotation allows retrieval of the getter value via Dart_GetField. Dart_Invoke can only be used with getters that return a closure value, in which case it is the same as retrieving the closure via Dart_GetField and then invoking the closure using Dart_InvokeClosure, so the "get" annotation is also needed for such uses.

If the second parameter is missing, null or true, it behaves the same as the "get" parameter.

Getters cannot be closurized.

Setters

Any one of the following forms may be attached to setters:

@pragma("vm:entry-point")
@pragma("vm:entry-point", true/false)
@pragma("vm:entry-point", !const bool.fromEnvironment("dart.vm.product"))
@pragma("vm:entry-point", "set")
void set foo(int value) { ... }

The "set" annotation allows setting the value via Dart_SetField.

If the second parameter is missing, null or true, it behaves the same as the "set" parameter.

Setters cannot be closurized.

Constructors

Any one of the following forms may be attached to constructors:

@pragma("vm:entry-point")
@pragma("vm:entry-point", true/false)
@pragma("vm:entry-point", !const bool.fromEnvironment("dart.vm.product"))
@pragma("vm:entry-point", "call")
C(this.foo) { ... }

If the annotation is "call", then the procedure is available for invocation (access via Dart_Invoke).

If the second parameter is missing, null or true, it behaves the same as the "call" parameter.

If the constructor is generative, the enclosing class must also be annotated for allocation from native or VM code.

Constructors cannot be closurized.

Other Procedures

Any one of the following forms may be attached to other types of procedures:

@pragma("vm:entry-point")
@pragma("vm:entry-point", true/false)
@pragma("vm:entry-point", !const bool.fromEnvironment("dart.vm.product"))
@pragma("vm:entry-point", "get")
@pragma("vm:entry-point", "call")
void foo(int value) { ... }

If the annotation is "get", then the procedure is only available for closurization (access via Dart_GetField).

If the annotation is "call", then the procedure is only available for invocation (access via Dart_Invoke).

If the second parameter is missing, null or true, the procedure is available for both closurization and invocation.

Fields

Any one of the following forms may be attached to a non-static field. The first three forms may be attached to static fields.

@pragma("vm:entry-point")
@pragma("vm:entry-point", null)
@pragma("vm:entry-point", true/false)
@pragma("vm:entry-point", !const bool.fromEnvironment("dart.vm.product"))
@pragma("vm:entry-point", "get"/"set")
int foo;

If the second parameter is missing, null or true, the field is marked for native access and for non-static fields the corresponding getter and setter in the interface of the enclosing class are marked for native invocation. If the "get" or "set" parameter is used, only the getter or setter is marked. For static fields, the implicit getter is always marked if the field is marked for native access.

A field containing a closure may only be invoked using Dart_Invoke if the getter is marked, in which case it is the same as retrieving the closure from the field using Dart_GetField and then invoking the closure using Dart_InvokeClosure.