[vm] Add @pragma('vm:keep-name') annotation

This allows us to keep symbol names for classes/methods/fields - even if
obfuscation is enabled, but has no other effect on tree shaker / ...

For go/dart-ama

TEST=vm/dart/keep_name_pragma_test

Change-Id: I66c0fc32217d9180f821658bae463f2c1d7fb1af
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/309740
Commit-Queue: Martin Kustermann <kustermann@google.com>
Reviewed-by: Slava Egorov <vegorov@google.com>
Auto-Submit: Martin Kustermann <kustermann@google.com>
This commit is contained in:
Martin Kustermann 2023-06-16 10:22:23 +00:00
parent 788a3a95c2
commit ddc236ba64
5 changed files with 128 additions and 8 deletions

View file

@ -18,6 +18,7 @@ import 'package:smith/smith.dart';
final Configuration _configuration = Configuration.parse(
const String.fromEnvironment("test_runner.configuration"),
<String, dynamic>{});
String configAsString = _configuration.toString();
bool get isDart2jsConfiguration => _configuration.compiler == Compiler.dart2js;

View file

@ -27,11 +27,11 @@ class ObfuscationProhibitionsVisitor extends RecursiveVisitor {
ObfuscationProhibitionsVisitor(this.parser);
void _addIfEntryPoint(
void _checkAnnotations(
List<Expression> annotations, String name, TreeNode node) {
for (var ann in annotations) {
ParsedPragma? pragma = parser.parsePragma(ann);
if (pragma is ParsedEntryPointPragma) {
for (final annotation in annotations) {
final pragma = parser.parsePragma(annotation);
if (pragma is ParsedEntryPointPragma || pragma is ParsedKeepNamePragma) {
metadata.protectedNames.add(name);
if (node is Field) {
metadata.protectedNames.add(name + "=");
@ -54,22 +54,22 @@ class ObfuscationProhibitionsVisitor extends RecursiveVisitor {
@override
visitClass(Class klass) {
_addIfEntryPoint(klass.annotations, klass.name, klass);
_checkAnnotations(klass.annotations, klass.name, klass);
klass.visitChildren(this);
}
@override
visitConstructor(Constructor ctor) {
_addIfEntryPoint(ctor.annotations, ctor.name.text, ctor);
_checkAnnotations(ctor.annotations, ctor.name.text, ctor);
}
@override
visitProcedure(Procedure proc) {
_addIfEntryPoint(proc.annotations, proc.name.text, proc);
_checkAnnotations(proc.annotations, proc.name.text, proc);
}
@override
visitField(Field field) {
_addIfEntryPoint(field.annotations, field.name.text, field);
_checkAnnotations(field.annotations, field.name.text, field);
}
}

View file

@ -14,6 +14,7 @@ const kResultTypeUsesPassedTypeArguments =
"result-type-uses-passed-type-arguments";
const kVmRecognizedPragmaName = "vm:recognized";
const kVmDisableUnboxedParametersPragmaName = "vm:disable-unboxed-parameters";
const kVmKeepNamePragmaName = "vm:keep-name";
// Pragmas recognized by dart2wasm
const kWasmEntryPointPragmaName = "wasm:entry-point";
@ -57,6 +58,10 @@ class ParsedDisableUnboxedParameters extends ParsedPragma {
const ParsedDisableUnboxedParameters();
}
class ParsedKeepNamePragma extends ParsedPragma {
const ParsedKeepNamePragma();
}
abstract class PragmaAnnotationParser {
/// May return 'null' if the annotation does not represent a recognized
/// @pragma.
@ -159,6 +164,8 @@ class ConstantPragmaAnnotationParser extends PragmaAnnotationParser {
return ParsedRecognized(type);
case kVmDisableUnboxedParametersPragmaName:
return const ParsedDisableUnboxedParameters();
case kVmKeepNamePragmaName:
return ParsedKeepNamePragma();
case kWasmEntryPointPragmaName:
return ParsedEntryPointPragma(PragmaEntryPointType.Default);
case kWasmExportPragmaName:

View file

@ -10,6 +10,7 @@ These pragmas are part of the VM's API and are safe for use in external code.
| `vm:never-inline` | [Never inline a function or method](compiler/pragmas_recognized_by_compiler.md#requesting-a-function-never-be-inlined) |
| `vm:prefer-inline` | [Inline a function or method when possible](compiler/pragmas_recognized_by_compiler.md#requesting-a-function-be-inlined) |
| `vm:notify-debugger-on-exception` | Marks a function that catches exceptions, making the VM treat any caught exception as if they were uncaught. This can be used to notify an attached debugger during debugging, without pausing the app during regular execution. |
| `vm:keep-name` | Will ensure we keep the name of the class/function - even if e.g. obfuscation mode is enabled. |
| `vm:external-name` | Allows to specify an external (native) name for an `external` function. This name is used to lookup native implementation via native resolver associated with the current library through embedding APIs. This is a replacement for legacy VM specific `native "name"` syntax. |
| `vm:invisible` | Allows to mark a function as invisible so it will not appear on stack traces. |
| `vm:always-consider-inlining` | Marks a function which particularly benefits from inlining and specialization in context of the caller (for example, when concrete types of arguments are known). Inliner will not give up after one failed inlining attempt and will continue trying to inline this function. |

View file

@ -0,0 +1,111 @@
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// VMOptions=--resolve-dwarf-paths --save-debugging-info=$TEST_COMPILATION_DIR/debug.so
import "dart:async";
import "dart:io";
import 'package:expect/expect.dart';
import 'package:native_stack_traces/native_stack_traces.dart';
import 'package:path/path.dart' as path;
final dwarfPath =
path.join(Platform.environment['TEST_COMPILATION_DIR']!, 'debug.so');
final usesObfuscation =
const String.fromEnvironment("test_runner.configuration")
.contains('obfuscate');
final usesDwarf =
const String.fromEnvironment("test_runner.configuration").contains('dwarf');
Future<void> main(List<String> args) async {
if (Platform.isAndroid) return;
final List<String> stack = await run(6);
final o = usesObfuscation ? '!' : '';
compareFrames([
'${o}bottom',
'KeepClass.keepMethod',
'${o}NormalClass.normalMethod',
'keepStatic',
'${o}normalStatic',
'${o}run',
], stack);
}
void compareFrames(List<String> patterns, List<String> stack) {
if (patterns.length != stack.length) {
throw 'Expected ${patterns.length} frames';
}
print('Comparing this pattern: \n ${patterns.join('\n ')}');
print('Against these frames: \n ${stack.join('\n ')}');
for (int i = 0; i < patterns.length; ++i) {
final pattern = patterns[i];
final frame = stack[i];
if (pattern.startsWith('!')) {
Expect.notEquals(pattern.substring(1), frame);
} else {
Expect.equals(pattern, frame);
}
}
print('');
}
@pragma('vm:never-inline')
void bottom() {
throw 'bad';
}
@pragma('vm:keep-name')
class KeepClass {
@pragma('vm:never-inline')
@pragma('vm:keep-name')
void keepMethod() {
bottom();
}
}
class NormalClass {
@pragma('vm:never-inline')
void normalMethod() {
keep.keepMethod();
}
}
final normal = NormalClass();
final keep = KeepClass();
@pragma('vm:never-inline')
@pragma('vm:keep-name')
void keepStatic() {
normal.normalMethod();
}
@pragma('vm:never-inline')
void normalStatic() {
keepStatic();
}
Future<List<String>> run(int n) async {
try {
normalStatic();
} catch (e, s) {
List<String> lines = s.toString().split('\n');
if (usesDwarf) {
final dwarf = Dwarf.fromFile(dwarfPath)!;
lines = await Stream<String>.fromIterable(lines)
.transform(DwarfStackTraceDecoder(dwarf))
.toList();
}
final start = lines.indexWhere((line) => line.startsWith('#0'));
lines = lines.skip(start).take(n).toList();
return lines.map((String line) {
line = line.substring(line.indexOf(' ')).trim();
line = line.substring(0, line.indexOf(' ')).trim();
return line;
}).toList();
}
throw 'failed';
}