From ddc236ba648bcbe025ed21109a2f7ef8367b5f48 Mon Sep 17 00:00:00 2001 From: Martin Kustermann Date: Fri, 16 Jun 2023 10:22:23 +0000 Subject: [PATCH] [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 Reviewed-by: Slava Egorov Auto-Submit: Martin Kustermann --- pkg/expect/lib/config.dart | 1 + .../obfuscation_prohibitions_annotator.dart | 16 +-- pkg/vm/lib/transformations/pragma.dart | 7 ++ runtime/docs/pragmas.md | 1 + .../tests/vm/dart/keep_name_pragma_test.dart | 111 ++++++++++++++++++ 5 files changed, 128 insertions(+), 8 deletions(-) create mode 100644 runtime/tests/vm/dart/keep_name_pragma_test.dart diff --git a/pkg/expect/lib/config.dart b/pkg/expect/lib/config.dart index cb6a3283823..d61821512ee 100644 --- a/pkg/expect/lib/config.dart +++ b/pkg/expect/lib/config.dart @@ -18,6 +18,7 @@ import 'package:smith/smith.dart'; final Configuration _configuration = Configuration.parse( const String.fromEnvironment("test_runner.configuration"), {}); +String configAsString = _configuration.toString(); bool get isDart2jsConfiguration => _configuration.compiler == Compiler.dart2js; diff --git a/pkg/vm/lib/transformations/obfuscation_prohibitions_annotator.dart b/pkg/vm/lib/transformations/obfuscation_prohibitions_annotator.dart index 762b17e7762..46f04ad90b7 100644 --- a/pkg/vm/lib/transformations/obfuscation_prohibitions_annotator.dart +++ b/pkg/vm/lib/transformations/obfuscation_prohibitions_annotator.dart @@ -27,11 +27,11 @@ class ObfuscationProhibitionsVisitor extends RecursiveVisitor { ObfuscationProhibitionsVisitor(this.parser); - void _addIfEntryPoint( + void _checkAnnotations( List 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); } } diff --git a/pkg/vm/lib/transformations/pragma.dart b/pkg/vm/lib/transformations/pragma.dart index 74d753cfd4a..999787aecb2 100644 --- a/pkg/vm/lib/transformations/pragma.dart +++ b/pkg/vm/lib/transformations/pragma.dart @@ -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: diff --git a/runtime/docs/pragmas.md b/runtime/docs/pragmas.md index 05df977d7df..340f1e0b586 100644 --- a/runtime/docs/pragmas.md +++ b/runtime/docs/pragmas.md @@ -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. | diff --git a/runtime/tests/vm/dart/keep_name_pragma_test.dart b/runtime/tests/vm/dart/keep_name_pragma_test.dart new file mode 100644 index 00000000000..9dd7e764f3e --- /dev/null +++ b/runtime/tests/vm/dart/keep_name_pragma_test.dart @@ -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 main(List args) async { + if (Platform.isAndroid) return; + + final List stack = await run(6); + final o = usesObfuscation ? '!' : ''; + + compareFrames([ + '${o}bottom', + 'KeepClass.keepMethod', + '${o}NormalClass.normalMethod', + 'keepStatic', + '${o}normalStatic', + '${o}run', + ], stack); +} + +void compareFrames(List patterns, List 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> run(int n) async { + try { + normalStatic(); + } catch (e, s) { + List lines = s.toString().split('\n'); + if (usesDwarf) { + final dwarf = Dwarf.fromFile(dwarfPath)!; + lines = await Stream.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'; +}