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'; +}