Reland "[vm/ffi] Express FFI call closures explicitly in AST"

This reverts commit 1800039c2af43501880e3f3774322c09423e9361.

The changes fd2e9b9f1af9e2ced5d263956f82e4ef3b583c8a and
c20f9eaf6ff3518b3d689abea411b7f9ca05dedb are relanded as is.

Reason for revert was fixed separately in
https://dart-review.googlesource.com/c/sdk/+/341621

TEST=ci

CoreLibraryReviewExempt: Implementation change only.
Issue: https://github.com/dart-lang/sdk/issues/54172
Issue: https://github.com/dart-lang/sdk/issues/39692
Change-Id: I1a2324768502e5ffbce328127938c0d3c96c38ba
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/341642
Reviewed-by: Siva Annamalai <asiva@google.com>
Commit-Queue: Alexander Markov <alexmarkov@google.com>
diff --git a/pkg/vm/lib/transformations/ffi/common.dart b/pkg/vm/lib/transformations/ffi/common.dart
index a41d7a0..2b664e4 100644
--- a/pkg/vm/lib/transformations/ffi/common.dart
+++ b/pkg/vm/lib/transformations/ffi/common.dart
@@ -236,7 +236,7 @@
   final Procedure abiSpecificIntegerArrayElemAt;
   final Procedure abiSpecificIntegerArraySetElemAt;
   final Procedure asFunctionMethod;
-  final Procedure asFunctionInternal;
+  final Procedure ffiCallMethod;
   final Procedure sizeOfMethod;
   final Procedure lookupFunctionMethod;
   final Procedure fromFunctionMethod;
@@ -289,6 +289,8 @@
   final Field nativeCallablePointerField;
   final Procedure nativeAddressOf;
   final Procedure nativePrivateAddressOf;
+  final Class ffiCallClass;
+  final Field ffiCallIsLeafField;
 
   late final InterfaceType nativeFieldWrapperClass1Type;
   late final InterfaceType voidType;
@@ -481,8 +483,7 @@
             index.getProcedure('dart:ffi', 'AbiSpecificIntegerArray', '[]='),
         asFunctionMethod = index.getProcedure(
             'dart:ffi', 'NativeFunctionPointer', 'asFunction'),
-        asFunctionInternal =
-            index.getTopLevelProcedure('dart:ffi', '_asFunctionInternal'),
+        ffiCallMethod = index.getTopLevelProcedure('dart:ffi', '_ffiCall'),
         sizeOfMethod = index.getTopLevelProcedure('dart:ffi', 'sizeOf'),
         lookupFunctionMethod = index.getProcedure(
             'dart:ffi', 'DynamicLibraryExtension', 'lookupFunction'),
@@ -589,7 +590,9 @@
         nativeAddressOf =
             index.getMember('dart:ffi', 'Native', 'addressOf') as Procedure,
         nativePrivateAddressOf =
-            index.getMember('dart:ffi', 'Native', '_addressOf') as Procedure {
+            index.getMember('dart:ffi', 'Native', '_addressOf') as Procedure,
+        ffiCallClass = index.getClass('dart:ffi', '_FfiCall'),
+        ffiCallIsLeafField = index.getField('dart:ffi', '_FfiCall', 'isLeaf') {
     nativeFieldWrapperClass1Type = nativeFieldWrapperClass1Class.getThisType(
         coreTypes, Nullability.nonNullable);
     voidType = nativeTypesClasses[NativeType.kVoid]!
@@ -1269,37 +1272,6 @@
       ..fileOffset = nestedExpression.fileOffset;
   }
 
-  /// Creates an invocation to asFunctionInternal.
-  ///
-  /// Adds a native effect invoking a compound constructors if this is used
-  /// as return type.
-  Expression buildAsFunctionInternal({
-    required Expression functionPointer,
-    required DartType nativeSignature,
-    required DartType dartSignature,
-    required bool isLeaf,
-    required int fileOffset,
-  }) {
-    final asFunctionInternalInvocation = StaticInvocation(
-        asFunctionInternal,
-        Arguments([
-          functionPointer,
-          BoolLiteral(isLeaf),
-        ], types: [
-          dartSignature,
-          nativeSignature,
-        ]))
-      ..fileOffset = fileOffset;
-
-    final possibleCompoundReturn = findCompoundReturnType(dartSignature);
-    if (possibleCompoundReturn != null) {
-      return invokeCompoundConstructor(
-          asFunctionInternalInvocation, possibleCompoundReturn);
-    }
-
-    return asFunctionInternalInvocation;
-  }
-
   /// Returns the compound [Class] if a compound is returned, otherwise `null`.
   Class? findCompoundReturnType(DartType dartSignature) {
     if (dartSignature is! FunctionType) {
diff --git a/pkg/vm/lib/transformations/ffi/use_sites.dart b/pkg/vm/lib/transformations/ffi/use_sites.dart
index 0d15a5a..a44f71c 100644
--- a/pkg/vm/lib/transformations/ffi/use_sites.dart
+++ b/pkg/vm/lib/transformations/ffi/use_sites.dart
@@ -106,9 +106,14 @@
   // callback.
   int callbackCount = 0;
 
+  // Used to create private top-level trampoline methods with unique names
+  // for each call.
+  int callCount = 0;
+
   @override
   TreeNode visitLibrary(Library node) {
     callbackCount = 0;
+    callCount = 0;
     return super.visitLibrary(node);
   }
 
@@ -360,10 +365,12 @@
         );
         final DartType nativeSignature = nativeType.typeArguments[0];
 
-        return buildAsFunctionInternal(
+        return _replaceAsFunction(
           functionPointer: node.arguments.positional[0],
+          pointerType: InterfaceType(
+              pointerClass, Nullability.nonNullable, [nativeType]),
           nativeSignature: nativeSignature,
-          dartSignature: dartType,
+          dartSignature: dartType as FunctionType,
           isLeaf: isLeaf,
           fileOffset: node.fileOffset,
         );
@@ -439,6 +446,84 @@
     return node;
   }
 
+  /// Create Dart function which calls native code.
+  ///
+  /// Adds a native effect invoking a compound constructors if this is used
+  /// as return type.
+  Expression _replaceAsFunction({
+    required Expression functionPointer,
+    required DartType pointerType,
+    required DartType nativeSignature,
+    required FunctionType dartSignature,
+    required bool isLeaf,
+    required int fileOffset,
+  }) {
+    assert(dartSignature.namedParameters.isEmpty);
+    final functionPointerVarName = '#ffiTarget$callCount';
+    final closureName = '#ffiClosure$callCount';
+    ++callCount;
+
+    final pointerVar = VariableDeclaration(functionPointerVarName,
+        initializer: functionPointer, type: pointerType, isSynthesized: true);
+
+    final positionalParameters = [
+      for (int i = 0; i < dartSignature.positionalParameters.length; ++i)
+        VariableDeclaration(
+          'arg${i + 1}',
+          type: dartSignature.positionalParameters[i],
+        )
+    ];
+
+    final closure = FunctionDeclaration(
+        VariableDeclaration(closureName,
+            type: dartSignature, isSynthesized: true)
+          ..addAnnotation(ConstantExpression(
+              InstanceConstant(coreTypes.pragmaClass.reference, [], {
+            coreTypes.pragmaName.fieldReference:
+                StringConstant('vm:ffi:call-closure'),
+            coreTypes.pragmaOptions.fieldReference: InstanceConstant(
+              ffiCallClass.reference,
+              [nativeSignature],
+              {
+                ffiCallIsLeafField.fieldReference: BoolConstant(isLeaf),
+              },
+            ),
+          }))),
+        FunctionNode(
+            Block([
+              for (final param in positionalParameters)
+                ExpressionStatement(StaticInvocation(
+                    nativeEffectMethod, Arguments([VariableGet(param)]))),
+              ReturnStatement(StaticInvocation(
+                  ffiCallMethod,
+                  Arguments([
+                    VariableGet(pointerVar),
+                  ], types: [
+                    dartSignature.returnType,
+                  ]))
+                ..fileOffset = fileOffset),
+            ]),
+            positionalParameters: positionalParameters,
+            requiredParameterCount: dartSignature.requiredParameterCount,
+            returnType: dartSignature.returnType)
+          ..fileOffset = fileOffset)
+      ..fileOffset = fileOffset;
+
+    final result = BlockExpression(
+        Block([
+          pointerVar,
+          closure,
+        ]),
+        VariableGet(closure.variable));
+
+    final possibleCompoundReturn = findCompoundReturnType(dartSignature);
+    if (possibleCompoundReturn != null) {
+      return invokeCompoundConstructor(result, possibleCompoundReturn);
+    }
+
+    return result;
+  }
+
   Expression invokeCompoundConstructors(
           Expression nestedExpression, List<Class> compoundClasses) =>
       compoundClasses
@@ -452,10 +537,6 @@
   // 'lookupFunction' are constants, so by inlining the call to 'asFunction' at
   // the call-site, we ensure that there are no generic calls to 'asFunction'.
   Expression _replaceLookupFunction(StaticInvocation node) {
-    // The generated code looks like:
-    //
-    // _asFunctionInternal<DS, NS>(lookup<NativeFunction<NS>>(symbolName),
-    //     isLeaf)
     final DartType nativeSignature = node.arguments.types[0];
     final DartType dartSignature = node.arguments.types[1];
 
@@ -468,21 +549,19 @@
     final FunctionType lookupFunctionType =
         libraryLookupMethod.getterType as FunctionType;
 
-    final Expression lookupResult = InstanceInvocation(
-        InstanceAccessKind.Instance,
-        node.arguments.positional[0],
-        libraryLookupMethod.name,
-        lookupArgs,
+    final lookupResult = InstanceInvocation(InstanceAccessKind.Instance,
+        node.arguments.positional[0], libraryLookupMethod.name, lookupArgs,
         interfaceTarget: libraryLookupMethod,
         functionType: FunctionTypeInstantiator.instantiate(
             lookupFunctionType, lookupTypeArgs));
 
     final isLeaf = getIsLeafBoolean(node) ?? false;
 
-    return buildAsFunctionInternal(
+    return _replaceAsFunction(
       functionPointer: lookupResult,
+      pointerType: lookupResult.functionType.returnType,
       nativeSignature: nativeSignature,
-      dartSignature: dartSignature,
+      dartSignature: dartSignature as FunctionType,
       isLeaf: isLeaf,
       fileOffset: node.fileOffset,
     );
diff --git a/pkg/vm/testcases/transformations/ffi/as_function.dart b/pkg/vm/testcases/transformations/ffi/as_function.dart
new file mode 100644
index 0000000..8bf0dc0
--- /dev/null
+++ b/pkg/vm/testcases/transformations/ffi/as_function.dart
@@ -0,0 +1,37 @@
+// 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.
+
+// Tests for NativeFunctionPointer.asFunction transformation.
+
+import 'dart:ffi';
+
+testVoidNoArg() {
+  final pointer =
+      Pointer<NativeFunction<Void Function()>>.fromAddress(0xdeadbeef);
+  final function = pointer.asFunction<void Function()>();
+  function();
+}
+
+testIntInt() {
+  final pointer =
+      Pointer<NativeFunction<Int32 Function(Int64)>>.fromAddress(0xdeadbeef);
+  final function = pointer.asFunction<int Function(int)>();
+  return function(42);
+}
+
+testLeaf5Args() {
+  final pointer = Pointer<
+      NativeFunction<
+          Int32 Function(
+              Int32, Int32, Int32, Int32, Int32)>>.fromAddress(0xdeadbeef);
+  final function =
+      pointer.asFunction<int Function(int, int, int, int, int)>(isLeaf: true);
+  return function(1, 2, 3, 4, 5);
+}
+
+void main() {
+  testVoidNoArg();
+  testIntInt();
+  testLeaf5Args();
+}
diff --git a/pkg/vm/testcases/transformations/ffi/as_function.dart.aot.expect b/pkg/vm/testcases/transformations/ffi/as_function.dart.aot.expect
new file mode 100644
index 0000000..233545a
--- /dev/null
+++ b/pkg/vm/testcases/transformations/ffi/as_function.dart.aot.expect
@@ -0,0 +1,63 @@
+library #lib;
+import self as self;
+import "dart:ffi" as ffi;
+import "dart:core" as core;
+import "dart:_internal" as _in;
+
+import "dart:ffi";
+
+static method testVoidNoArg() → dynamic {
+  final ffi::Pointer<ffi::NativeFunction<() → ffi::Void>> pointer = [@vm.inferred-type.metadata=dart.ffi::Pointer] ffi::Pointer::fromAddress<ffi::NativeFunction<() → ffi::Void>>(3735928559);
+  final () → void function = block {
+    [@vm.inferred-type.metadata=dart.ffi::Pointer] synthesized ffi::Pointer<ffi::NativeFunction<() → ffi::Void>> #ffiTarget0 = pointer;
+    @#C4
+    function #ffiClosure0() → void {
+      return ffi::_ffiCall<void>(#ffiTarget0);
+    }
+  } =>#ffiClosure0;
+  function(){() → void};
+}
+[@vm.unboxing-info.metadata=()->i]static method testIntInt() → dynamic {
+  final ffi::Pointer<ffi::NativeFunction<(ffi::Int64) → ffi::Int32>> pointer = [@vm.inferred-type.metadata=dart.ffi::Pointer] ffi::Pointer::fromAddress<ffi::NativeFunction<(ffi::Int64) → ffi::Int32>>(3735928559);
+  final (core::int) → core::int function = block {
+    [@vm.inferred-type.metadata=dart.ffi::Pointer] synthesized ffi::Pointer<ffi::NativeFunction<(ffi::Int64) → ffi::Int32>> #ffiTarget1 = pointer;
+    @#C6
+    function #ffiClosure1(core::int arg1) → core::int {
+      _in::_nativeEffect(arg1);
+      return ffi::_ffiCall<core::int>(#ffiTarget1);
+    }
+  } =>#ffiClosure1;
+  return function(42){(core::int) → core::int};
+}
+[@vm.unboxing-info.metadata=()->i]static method testLeaf5Args() → dynamic {
+  final ffi::Pointer<ffi::NativeFunction<(ffi::Int32, ffi::Int32, ffi::Int32, ffi::Int32, ffi::Int32) → ffi::Int32>> pointer = [@vm.inferred-type.metadata=dart.ffi::Pointer] ffi::Pointer::fromAddress<ffi::NativeFunction<(ffi::Int32, ffi::Int32, ffi::Int32, ffi::Int32, ffi::Int32) → ffi::Int32>>(3735928559);
+  final (core::int, core::int, core::int, core::int, core::int) → core::int function = block {
+    [@vm.inferred-type.metadata=dart.ffi::Pointer] synthesized ffi::Pointer<ffi::NativeFunction<(ffi::Int32, ffi::Int32, ffi::Int32, ffi::Int32, ffi::Int32) → ffi::Int32>> #ffiTarget2 = pointer;
+    @#C9
+    function #ffiClosure2(core::int arg1, core::int arg2, core::int arg3, core::int arg4, core::int arg5) → core::int {
+      _in::_nativeEffect(arg1);
+      _in::_nativeEffect(arg2);
+      _in::_nativeEffect(arg3);
+      _in::_nativeEffect(arg4);
+      _in::_nativeEffect(arg5);
+      return ffi::_ffiCall<core::int>(#ffiTarget2);
+    }
+  } =>#ffiClosure2;
+  return function(1, 2, 3, 4, 5){(core::int, core::int, core::int, core::int, core::int) → core::int};
+}
+static method main() → void {
+  self::testVoidNoArg();
+  self::testIntInt();
+  self::testLeaf5Args();
+}
+constants  {
+  #C1 = "vm:ffi:call-closure"
+  #C2 = false
+  #C3 = ffi::_FfiCall<() → ffi::Void> {isLeaf:#C2}
+  #C4 = core::pragma {name:#C1, options:#C3}
+  #C5 = ffi::_FfiCall<(ffi::Int64) → ffi::Int32> {isLeaf:#C2}
+  #C6 = core::pragma {name:#C1, options:#C5}
+  #C7 = true
+  #C8 = ffi::_FfiCall<(ffi::Int32, ffi::Int32, ffi::Int32, ffi::Int32, ffi::Int32) → ffi::Int32> {isLeaf:#C7}
+  #C9 = core::pragma {name:#C1, options:#C8}
+}
diff --git a/pkg/vm/testcases/transformations/ffi/as_function.dart.expect b/pkg/vm/testcases/transformations/ffi/as_function.dart.expect
new file mode 100644
index 0000000..4bd77d2
--- /dev/null
+++ b/pkg/vm/testcases/transformations/ffi/as_function.dart.expect
@@ -0,0 +1,63 @@
+library #lib;
+import self as self;
+import "dart:ffi" as ffi;
+import "dart:core" as core;
+import "dart:_internal" as _in;
+
+import "dart:ffi";
+
+static method testVoidNoArg() → dynamic {
+  final ffi::Pointer<ffi::NativeFunction<() → ffi::Void>> pointer = ffi::Pointer::fromAddress<ffi::NativeFunction<() → ffi::Void>>(3735928559);
+  final () → void function = block {
+    synthesized ffi::Pointer<ffi::NativeFunction<() → ffi::Void>> #ffiTarget0 = pointer;
+    @#C4
+    function #ffiClosure0() → void {
+      return ffi::_ffiCall<void>(#ffiTarget0);
+    }
+  } =>#ffiClosure0;
+  function(){() → void};
+}
+static method testIntInt() → dynamic {
+  final ffi::Pointer<ffi::NativeFunction<(ffi::Int64) → ffi::Int32>> pointer = ffi::Pointer::fromAddress<ffi::NativeFunction<(ffi::Int64) → ffi::Int32>>(3735928559);
+  final (core::int) → core::int function = block {
+    synthesized ffi::Pointer<ffi::NativeFunction<(ffi::Int64) → ffi::Int32>> #ffiTarget1 = pointer;
+    @#C6
+    function #ffiClosure1(core::int arg1) → core::int {
+      _in::_nativeEffect(arg1);
+      return ffi::_ffiCall<core::int>(#ffiTarget1);
+    }
+  } =>#ffiClosure1;
+  return function(42){(core::int) → core::int};
+}
+static method testLeaf5Args() → dynamic {
+  final ffi::Pointer<ffi::NativeFunction<(ffi::Int32, ffi::Int32, ffi::Int32, ffi::Int32, ffi::Int32) → ffi::Int32>> pointer = ffi::Pointer::fromAddress<ffi::NativeFunction<(ffi::Int32, ffi::Int32, ffi::Int32, ffi::Int32, ffi::Int32) → ffi::Int32>>(3735928559);
+  final (core::int, core::int, core::int, core::int, core::int) → core::int function = block {
+    synthesized ffi::Pointer<ffi::NativeFunction<(ffi::Int32, ffi::Int32, ffi::Int32, ffi::Int32, ffi::Int32) → ffi::Int32>> #ffiTarget2 = pointer;
+    @#C9
+    function #ffiClosure2(core::int arg1, core::int arg2, core::int arg3, core::int arg4, core::int arg5) → core::int {
+      _in::_nativeEffect(arg1);
+      _in::_nativeEffect(arg2);
+      _in::_nativeEffect(arg3);
+      _in::_nativeEffect(arg4);
+      _in::_nativeEffect(arg5);
+      return ffi::_ffiCall<core::int>(#ffiTarget2);
+    }
+  } =>#ffiClosure2;
+  return function(1, 2, 3, 4, 5){(core::int, core::int, core::int, core::int, core::int) → core::int};
+}
+static method main() → void {
+  self::testVoidNoArg();
+  self::testIntInt();
+  self::testLeaf5Args();
+}
+constants  {
+  #C1 = "vm:ffi:call-closure"
+  #C2 = false
+  #C3 = ffi::_FfiCall<() → ffi::Void> {isLeaf:#C2}
+  #C4 = core::pragma {name:#C1, options:#C3}
+  #C5 = ffi::_FfiCall<(ffi::Int64) → ffi::Int32> {isLeaf:#C2}
+  #C6 = core::pragma {name:#C1, options:#C5}
+  #C7 = true
+  #C8 = ffi::_FfiCall<(ffi::Int32, ffi::Int32, ffi::Int32, ffi::Int32, ffi::Int32) → ffi::Int32> {isLeaf:#C7}
+  #C9 = core::pragma {name:#C1, options:#C8}
+}
diff --git a/pkg/vm/testcases/transformations/type_flow/transformer/ffi_struct_constructors.dart.expect b/pkg/vm/testcases/transformations/type_flow/transformer/ffi_struct_constructors.dart.expect
index f6c28c4..2e9b3d2 100644
--- a/pkg/vm/testcases/transformations/type_flow/transformer/ffi_struct_constructors.dart.expect
+++ b/pkg/vm/testcases/transformations/type_flow/transformer/ffi_struct_constructors.dart.expect
@@ -67,7 +67,13 @@
   final ffi::DynamicLibrary dylib = [@vm.inferred-type.metadata=dart.ffi::DynamicLibrary] ffi::DynamicLibrary::executable();
   final () → self::Struct1 function1 = block {
     _in::_nativeEffect(new self::Struct1::#fromTypedDataBase([@vm.inferred-type.metadata=dart.typed_data::_Uint8List] typ::Uint8List::•(#C18)));
-  } =>ffi::_asFunctionInternal<() → self::Struct1, () → self::Struct1>([@vm.direct-call.metadata=dart.ffi::DynamicLibrary.lookup] [@vm.inferred-type.metadata=dart.ffi::Pointer (skip check)] dylib.{ffi::DynamicLibrary::lookup}<ffi::NativeFunction<() → self::Struct1>>("function1"){(core::String) → ffi::Pointer<ffi::NativeFunction<() → self::Struct1>>}, false);
+  } => block {
+    [@vm.inferred-type.metadata=dart.ffi::Pointer] synthesized ffi::Pointer<ffi::NativeFunction<() → self::Struct1>> #ffiTarget0 = [@vm.direct-call.metadata=dart.ffi::DynamicLibrary.lookup] [@vm.inferred-type.metadata=dart.ffi::Pointer (skip check)] dylib.{ffi::DynamicLibrary::lookup}<ffi::NativeFunction<() → self::Struct1>>("function1"){(core::String) → ffi::Pointer<ffi::NativeFunction<() → self::Struct1>>};
+    @#C22
+    function #ffiClosure0() → self::Struct1 {
+      return ffi::_ffiCall<self::Struct1>(#ffiTarget0);
+    }
+  } =>#ffiClosure0;
   final self::Struct1 struct1 = function1(){() → self::Struct1};
   core::print(struct1);
 }
@@ -75,7 +81,13 @@
   final ffi::Pointer<ffi::NativeFunction<() → self::Struct2>> pointer = [@vm.inferred-type.metadata=dart.ffi::Pointer] ffi::Pointer::fromAddress<ffi::NativeFunction<() → self::Struct2>>(3735928559);
   final () → self::Struct2 function2 = block {
     _in::_nativeEffect(new self::Struct2::#fromTypedDataBase([@vm.inferred-type.metadata=dart.typed_data::_Uint8List] typ::Uint8List::•(#C18)));
-  } =>ffi::_asFunctionInternal<() → self::Struct2, () → self::Struct2>(pointer, false);
+  } => block {
+    [@vm.inferred-type.metadata=dart.ffi::Pointer] synthesized ffi::Pointer<ffi::NativeFunction<() → self::Struct2>> #ffiTarget1 = pointer;
+    @#C24
+    function #ffiClosure1() → self::Struct2 {
+      return ffi::_ffiCall<self::Struct2>(#ffiTarget1);
+    }
+  } =>#ffiClosure1;
   final self::Struct2 struct2 = function2(){() → self::Struct2};
   core::print(struct2);
 }
@@ -90,12 +102,26 @@
 }
 static method testLookupFunctionArgument() → void {
   final ffi::DynamicLibrary dylib = [@vm.inferred-type.metadata=dart.ffi::DynamicLibrary] ffi::DynamicLibrary::executable();
-  final (self::Struct5) → void function5 = [@vm.inferred-type.metadata=dart.core::_Closure] ffi::_asFunctionInternal<(self::Struct5) → void, (self::Struct5) → ffi::Void>([@vm.direct-call.metadata=dart.ffi::DynamicLibrary.lookup] [@vm.inferred-type.metadata=dart.ffi::Pointer (skip check)] dylib.{ffi::DynamicLibrary::lookup}<ffi::NativeFunction<(self::Struct5) → ffi::Void>>("function5"){(core::String) → ffi::Pointer<ffi::NativeFunction<(self::Struct5) → ffi::Void>>}, false);
+  final (self::Struct5) → void function5 = block {
+    [@vm.inferred-type.metadata=dart.ffi::Pointer] synthesized ffi::Pointer<ffi::NativeFunction<(self::Struct5) → ffi::Void>> #ffiTarget2 = [@vm.direct-call.metadata=dart.ffi::DynamicLibrary.lookup] [@vm.inferred-type.metadata=dart.ffi::Pointer (skip check)] dylib.{ffi::DynamicLibrary::lookup}<ffi::NativeFunction<(self::Struct5) → ffi::Void>>("function5"){(core::String) → ffi::Pointer<ffi::NativeFunction<(self::Struct5) → ffi::Void>>};
+    @#C26
+    function #ffiClosure2(self::Struct5 arg1) → void {
+      _in::_nativeEffect(arg1);
+      return ffi::_ffiCall<void>(#ffiTarget2);
+    }
+  } =>#ffiClosure2;
   core::print(function5);
 }
 static method testAsFunctionArgument() → void {
   final ffi::Pointer<ffi::NativeFunction<(self::Struct6) → ffi::Void>> pointer = [@vm.inferred-type.metadata=dart.ffi::Pointer] ffi::Pointer::fromAddress<ffi::NativeFunction<(self::Struct6) → ffi::Void>>(3735928559);
-  final (self::Struct6) → void function6 = [@vm.inferred-type.metadata=dart.core::_Closure] ffi::_asFunctionInternal<(self::Struct6) → void, (self::Struct6) → ffi::Void>(pointer, false);
+  final (self::Struct6) → void function6 = block {
+    [@vm.inferred-type.metadata=dart.ffi::Pointer] synthesized ffi::Pointer<ffi::NativeFunction<(self::Struct6) → ffi::Void>> #ffiTarget3 = pointer;
+    @#C28
+    function #ffiClosure3(self::Struct6 arg1) → void {
+      _in::_nativeEffect(arg1);
+      return ffi::_ffiCall<void>(#ffiTarget3);
+    }
+  } =>#ffiClosure3;
   core::print(function6);
 }
 static method returnStruct7() → self::Struct7 {
@@ -135,4 +161,14 @@
   #C16 = static-tearoff self::useStruct3
   #C17 = static-tearoff self::returnStruct7
   #C18 = 1
+  #C19 = "vm:ffi:call-closure"
+  #C20 = false
+  #C21 = ffi::_FfiCall<() → self::Struct1> {isLeaf:#C20}
+  #C22 = core::pragma {name:#C19, options:#C21}
+  #C23 = ffi::_FfiCall<() → self::Struct2> {isLeaf:#C20}
+  #C24 = core::pragma {name:#C19, options:#C23}
+  #C25 = ffi::_FfiCall<(self::Struct5) → ffi::Void> {isLeaf:#C20}
+  #C26 = core::pragma {name:#C19, options:#C25}
+  #C27 = ffi::_FfiCall<(self::Struct6) → ffi::Void> {isLeaf:#C20}
+  #C28 = core::pragma {name:#C19, options:#C27}
 }
diff --git a/runtime/docs/compiler/ffi_pragmas.md b/runtime/docs/compiler/ffi_pragmas.md
index 545e8ed..6240f55 100644
--- a/runtime/docs/compiler/ffi_pragmas.md
+++ b/runtime/docs/compiler/ffi_pragmas.md
@@ -45,3 +45,11 @@
 * [runtime/vm/kernel_loader.cc](../../../runtime/vm/kernel_loader.cc)
 * [runtime/vm/object.cc](../../../runtime/vm/object.cc)
 
+## FFI Calls
+
+This pragma is used to mark Dart closures which perform FFI calls:
+
+```
+  @pragma('vm:ffi:call-closure', _FfiCall<Int32 Function(Int32)>(isLeaf: false))
+  int #ffiCall0(int arg1) => _ffiCall<int>(target);
+```
diff --git a/runtime/docs/pragmas.md b/runtime/docs/pragmas.md
index 120db16..4e274b4 100644
--- a/runtime/docs/pragmas.md
+++ b/runtime/docs/pragmas.md
@@ -47,6 +47,7 @@
 
 | Pragma | Meaning |
 | --- | --- |
+| `vm:ffi:call-closure`| [Closure performing FFI calls](compiler/ffi_pragmas.md) |
 | `vm:ffi:native-assets` | [Passing a native assets mapping to the VM](compiler/ffi_pragmas.md) |
 | `vm:ffi:native`| [Passing a native arguments to the VM](compiler/ffi_pragmas.md) |
 
diff --git a/runtime/lib/ffi.cc b/runtime/lib/ffi.cc
index 799dd78..0d06325 100644
--- a/runtime/lib/ffi.cc
+++ b/runtime/lib/ffi.cc
@@ -20,7 +20,6 @@
 
 #if !defined(DART_PRECOMPILED_RUNTIME)
 #include "vm/compiler/assembler/assembler.h"
-#include "vm/compiler/ffi/call.h"
 #include "vm/compiler/ffi/callback.h"
 #include "vm/compiler/ffi/marshaller.h"
 #include "vm/compiler/jit/compiler.h"
@@ -28,11 +27,6 @@
 
 namespace dart {
 
-// Static invocations to this method are translated directly in streaming FGB.
-DEFINE_NATIVE_ENTRY(Ffi_asFunctionInternal, 2, 2) {
-  UNREACHABLE();
-}
-
 DEFINE_NATIVE_ENTRY(Ffi_createNativeCallableListener, 1, 2) {
   const auto& send_function =
       Function::CheckedHandle(zone, arguments->NativeArg0());
diff --git a/runtime/vm/bootstrap_natives.h b/runtime/vm/bootstrap_natives.h
index b9e42ce..11be9be 100644
--- a/runtime/vm/bootstrap_natives.h
+++ b/runtime/vm/bootstrap_natives.h
@@ -321,7 +321,6 @@
   V(VMService_DecodeAssets, 1)                                                 \
   V(VMService_AddUserTagsToStreamableSampleList, 1)                            \
   V(VMService_RemoveUserTagsFromStreamableSampleList, 1)                       \
-  V(Ffi_asFunctionInternal, 2)                                                 \
   V(Ffi_createNativeCallableListener, 2)                                       \
   V(Ffi_createNativeCallableIsolateLocal, 3)                                   \
   V(Ffi_deleteNativeCallable, 1)                                               \
diff --git a/runtime/vm/canonical_tables.h b/runtime/vm/canonical_tables.h
index 70e4618..0b0f5ef 100644
--- a/runtime/vm/canonical_tables.h
+++ b/runtime/vm/canonical_tables.h
@@ -425,7 +425,7 @@
             f1.FfiCSignature() == f2.FfiCSignature() &&
             f1.FfiCallbackExceptionalReturn() ==
                 f2.FfiCallbackExceptionalReturn() &&
-            f1.GetFfiFunctionKind() == f2.GetFfiFunctionKind());
+            f1.GetFfiCallbackKind() == f2.GetFfiCallbackKind());
   }
   static bool ReportStats() { return false; }
 };
diff --git a/runtime/vm/compiler/aot/precompiler.cc b/runtime/vm/compiler/aot/precompiler.cc
index b98a9c4..2a28718 100644
--- a/runtime/vm/compiler/aot/precompiler.cc
+++ b/runtime/vm/compiler/aot/precompiler.cc
@@ -1044,7 +1044,7 @@
       // Local closure function.
       const auto& target = Function::Cast(entry);
       AddFunction(target, RetainReasons::kLocalClosure);
-      if (target.IsFfiTrampoline()) {
+      if (target.IsFfiCallbackTrampoline()) {
         const auto& callback_target =
             Function::Handle(Z, target.FfiCallbackTarget());
         if (!callback_target.IsNull()) {
@@ -1117,7 +1117,7 @@
   const Class& owner = Class::Handle(Z, function.Owner());
   AddTypesOf(owner);
 
-  if (function.IsFfiTrampoline()) {
+  if (function.IsFfiCallbackTrampoline()) {
     AddType(FunctionType::Handle(Z, function.FfiCSignature()));
   }
 
@@ -2030,7 +2030,7 @@
     // Ffi trampoline functions are not reachable from program structure,
     // they are referenced only from code (object pool).
     if (!functions_to_retain_.ContainsKey(function) &&
-        !function.IsFfiTrampoline()) {
+        !function.IsFfiCallbackTrampoline()) {
       FATAL("Function %s was not traced in TraceForRetainedFunctions\n",
             function.ToFullyQualifiedCString());
     }
@@ -2189,7 +2189,7 @@
       // which need the signature.
       return AddRetainReason(sig, RetainReasons::kClosureSignature);
     }
-    if (function.IsFfiTrampoline()) {
+    if (function.IsFfiCallbackTrampoline()) {
       // FFI trampolines may be dynamically called.
       return AddRetainReason(sig, RetainReasons::kFfiTrampolineSignature);
     }
@@ -3015,7 +3015,7 @@
       }
 
       // Retain Code objects corresponding to FFI trampolines.
-      if (function_.IsFfiTrampoline()) {
+      if (function_.IsFfiCallbackTrampoline()) {
         ++codes_with_ffi_trampoline_function_;
         return;
       }
@@ -3455,8 +3455,7 @@
     function.AttachCode(code);
   }
 
-  if (function.IsFfiTrampoline() &&
-      function.GetFfiFunctionKind() != FfiFunctionKind::kCall) {
+  if (function.IsFfiCallbackTrampoline()) {
     compiler::ffi::SetFfiCallbackCode(thread(), function, code);
   }
 }
diff --git a/runtime/vm/compiler/backend/flow_graph.cc b/runtime/vm/compiler/backend/flow_graph.cc
index 1688efe..b208aac 100644
--- a/runtime/vm/compiler/backend/flow_graph.cc
+++ b/runtime/vm/compiler/backend/flow_graph.cc
@@ -167,7 +167,7 @@
 bool FlowGraph::ShouldReorderBlocks(const Function& function,
                                     bool is_optimized) {
   return is_optimized && FLAG_reorder_basic_blocks &&
-         !function.is_intrinsic() && !function.IsFfiTrampoline();
+         !function.is_intrinsic() && !function.IsFfiCallbackTrampoline();
 }
 
 GrowableArray<BlockEntryInstr*>* FlowGraph::CodegenBlockOrder(
diff --git a/runtime/vm/compiler/backend/il_serializer.cc b/runtime/vm/compiler/backend/il_serializer.cc
index b2d5dfc..d3cbf79 100644
--- a/runtime/vm/compiler/backend/il_serializer.cc
+++ b/runtime/vm/compiler/backend/il_serializer.cc
@@ -11,7 +11,6 @@
 #include "vm/compiler/backend/flow_graph.h"
 #include "vm/compiler/backend/il.h"
 #include "vm/compiler/backend/range_analysis.h"
-#include "vm/compiler/ffi/call.h"
 #include "vm/compiler/frontend/flow_graph_builder.h"
 #include "vm/object_store.h"
 #include "vm/parser.h"
@@ -837,20 +836,12 @@
       return;
     }
     case UntaggedFunction::kFfiTrampoline: {
-      s->Write<uint8_t>(static_cast<uint8_t>(x.GetFfiFunctionKind()));
+      s->Write<uint8_t>(static_cast<uint8_t>(x.GetFfiCallbackKind()));
       s->Write<const FunctionType&>(
           FunctionType::Handle(zone, x.FfiCSignature()));
-      if (x.GetFfiFunctionKind() != FfiFunctionKind::kCall) {
-        s->Write<const Function&>(
-            Function::Handle(zone, x.FfiCallbackTarget()));
-        s->Write<const Instance&>(
-            Instance::Handle(zone, x.FfiCallbackExceptionalReturn()));
-      } else {
-        s->Write<const String&>(String::Handle(zone, x.name()));
-        s->Write<const FunctionType&>(
-            FunctionType::Handle(zone, x.signature()));
-        s->Write<bool>(x.FfiIsLeaf());
-      }
+      s->Write<const Function&>(Function::Handle(zone, x.FfiCallbackTarget()));
+      s->Write<const Instance&>(
+          Instance::Handle(zone, x.FfiCallbackExceptionalReturn()));
       return;
     }
     default:
@@ -927,23 +918,14 @@
                                   target.GetDynamicInvocationForwarder(name));
     }
     case UntaggedFunction::kFfiTrampoline: {
-      const FfiFunctionKind kind =
-          static_cast<FfiFunctionKind>(d->Read<uint8_t>());
+      const FfiCallbackKind kind =
+          static_cast<FfiCallbackKind>(d->Read<uint8_t>());
       const FunctionType& c_signature = d->Read<const FunctionType&>();
-      if (kind != FfiFunctionKind::kCall) {
-        const Function& callback_target = d->Read<const Function&>();
-        const Instance& exceptional_return = d->Read<const Instance&>();
-        return Function::ZoneHandle(
-            zone, compiler::ffi::NativeCallbackFunction(
-                      c_signature, callback_target, exceptional_return, kind));
-      } else {
-        const String& name = d->Read<const String&>();
-        const FunctionType& signature = d->Read<const FunctionType&>();
-        const bool is_leaf = d->Read<bool>();
-        return Function::ZoneHandle(
-            zone, compiler::ffi::TrampolineFunction(name, signature,
-                                                    c_signature, is_leaf));
-      }
+      const Function& callback_target = d->Read<const Function&>();
+      const Instance& exceptional_return = d->Read<const Instance&>();
+      return Function::ZoneHandle(
+          zone, compiler::ffi::NativeCallbackFunction(
+                    c_signature, callback_target, exceptional_return, kind));
     }
     default:
       UNIMPLEMENTED();
diff --git a/runtime/vm/compiler/compiler_sources.gni b/runtime/vm/compiler/compiler_sources.gni
index 9a051b6e..997bb8d 100644
--- a/runtime/vm/compiler/compiler_sources.gni
+++ b/runtime/vm/compiler/compiler_sources.gni
@@ -100,8 +100,6 @@
   "compiler_timings.h",
   "ffi/abi.cc",
   "ffi/abi.h",
-  "ffi/call.cc",
-  "ffi/call.h",
   "ffi/callback.cc",
   "ffi/callback.h",
   "ffi/frame_rebase.cc",
diff --git a/runtime/vm/compiler/ffi/call.cc b/runtime/vm/compiler/ffi/call.cc
deleted file mode 100644
index faf6936..0000000
--- a/runtime/vm/compiler/ffi/call.cc
+++ /dev/null
@@ -1,83 +0,0 @@
-// Copyright (c) 2020, 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.
-
-#include "vm/compiler/ffi/call.h"
-
-#include "vm/class_finalizer.h"
-#include "vm/symbols.h"
-
-namespace dart {
-
-namespace compiler {
-
-namespace ffi {
-
-// TODO(dartbug.com/36607): Cache the trampolines.
-FunctionPtr TrampolineFunction(const String& name,
-                               const FunctionType& signature,
-                               const FunctionType& c_signature,
-                               bool is_leaf) {
-  ASSERT(signature.num_implicit_parameters() == 1);
-  Thread* thread = Thread::Current();
-  Zone* zone = thread->zone();
-  const Library& lib = Library::Handle(zone, Library::FfiLibrary());
-  const Class& owner_class = Class::Handle(zone, lib.toplevel_class());
-  Function& function = Function::Handle(
-      zone, Function::New(signature, name, UntaggedFunction::kFfiTrampoline,
-                          /*is_static=*/true,
-                          /*is_const=*/false,
-                          /*is_abstract=*/false,
-                          /*is_external=*/false,
-                          /*is_native=*/false, owner_class,
-                          TokenPosition::kMinSource));
-  function.set_is_debuggable(false);
-
-  // Create unique names for the parameters, as they are used in scope building
-  // and error messages.
-  if (signature.num_fixed_parameters() > 0) {
-    function.CreateNameArray();
-    function.SetParameterNameAt(0, Symbols::ClosureParameter());
-    auto& param_name = String::Handle(zone);
-    for (intptr_t i = 1, n = signature.num_fixed_parameters(); i < n; ++i) {
-      param_name = Symbols::NewFormatted(thread, ":ffi_param%" Pd, i);
-      function.SetParameterNameAt(i, param_name);
-    }
-  }
-
-  function.SetFfiCSignature(c_signature);
-  function.SetFfiIsLeaf(is_leaf);
-  function.SetFfiFunctionKind(FfiFunctionKind::kCall);
-
-  return function.ptr();
-}
-
-FunctionPtr TrampolineFunction(const FunctionType& dart_signature,
-                               const FunctionType& c_signature,
-                               bool is_leaf,
-                               const String& function_name) {
-  Thread* thread = Thread::Current();
-  Zone* zone = thread->zone();
-  String& name =
-      String::Handle(zone, Symbols::NewFormatted(thread, "FfiTrampoline_%s",
-                                                 function_name.ToCString()));
-
-  // Trampolines have no optional arguments.
-  FunctionType& signature = FunctionType::Handle(zone, FunctionType::New());
-  const intptr_t num_fixed = dart_signature.num_fixed_parameters();
-  signature.set_num_implicit_parameters(1);
-  signature.set_num_fixed_parameters(num_fixed);
-  signature.set_result_type(
-      AbstractType::Handle(zone, dart_signature.result_type()));
-  signature.set_parameter_types(
-      Array::Handle(zone, dart_signature.parameter_types()));
-  signature ^= ClassFinalizer::FinalizeType(signature);
-
-  return TrampolineFunction(name, signature, c_signature, is_leaf);
-}
-
-}  // namespace ffi
-
-}  // namespace compiler
-
-}  // namespace dart
diff --git a/runtime/vm/compiler/ffi/call.h b/runtime/vm/compiler/ffi/call.h
deleted file mode 100644
index 11c328a..0000000
--- a/runtime/vm/compiler/ffi/call.h
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright (c) 2020, 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.
-
-#ifndef RUNTIME_VM_COMPILER_FFI_CALL_H_
-#define RUNTIME_VM_COMPILER_FFI_CALL_H_
-
-#if defined(DART_PRECOMPILED_RUNTIME)
-#error "AOT runtime should not use compiler sources (including header files)"
-#endif  // defined(DART_PRECOMPILED_RUNTIME)
-
-#include <platform/globals.h>
-
-#include "vm/raw_object.h"
-
-namespace dart {
-
-namespace compiler {
-
-namespace ffi {
-
-FunctionPtr TrampolineFunction(const String& name,
-                               const FunctionType& signature,
-                               const FunctionType& c_signature,
-                               bool is_leaf);
-
-FunctionPtr TrampolineFunction(const FunctionType& dart_signature,
-                               const FunctionType& c_signature,
-                               bool is_leaf,
-                               const String& function_name);
-
-}  // namespace ffi
-
-}  // namespace compiler
-
-}  // namespace dart
-
-#endif  // RUNTIME_VM_COMPILER_FFI_CALL_H_
diff --git a/runtime/vm/compiler/ffi/callback.cc b/runtime/vm/compiler/ffi/callback.cc
index 7a9ea1c..984b2a0 100644
--- a/runtime/vm/compiler/ffi/callback.cc
+++ b/runtime/vm/compiler/ffi/callback.cc
@@ -18,13 +18,13 @@
 const String& NativeCallbackFunctionName(Thread* thread,
                                          Zone* zone,
                                          const Function& dart_target,
-                                         FfiFunctionKind kind) {
+                                         FfiCallbackKind kind) {
   switch (kind) {
-    case FfiFunctionKind::kAsyncCallback:
+    case FfiCallbackKind::kAsyncCallback:
       return Symbols::FfiAsyncCallback();
-    case FfiFunctionKind::kIsolateLocalClosureCallback:
+    case FfiCallbackKind::kIsolateLocalClosureCallback:
       return Symbols::FfiIsolateLocalCallback();
-    case FfiFunctionKind::kIsolateLocalStaticCallback:
+    case FfiCallbackKind::kIsolateLocalStaticCallback:
       return String::Handle(
           zone, Symbols::FromConcat(thread, Symbols::FfiCallback(),
                                     String::Handle(zone, dart_target.name())));
@@ -36,7 +36,7 @@
 FunctionPtr NativeCallbackFunction(const FunctionType& c_signature,
                                    const Function& dart_target,
                                    const Instance& exceptional_return,
-                                   FfiFunctionKind kind) {
+                                   FfiCallbackKind kind) {
   Thread* const thread = Thread::Current();
   Zone* const zone = thread->zone();
   Function& function = Function::Handle(zone);
@@ -64,7 +64,7 @@
   // the body.
   function.SetFfiCSignature(c_signature);
   function.SetFfiCallbackTarget(dart_target);
-  function.SetFfiFunctionKind(kind);
+  function.SetFfiCallbackKind(kind);
 
   // We need to load the exceptional return value as a constant in the generated
   // function. Even though the FE ensures that it is a constant, it could still
diff --git a/runtime/vm/compiler/ffi/callback.h b/runtime/vm/compiler/ffi/callback.h
index 88af9a99..02b4294 100644
--- a/runtime/vm/compiler/ffi/callback.h
+++ b/runtime/vm/compiler/ffi/callback.h
@@ -23,7 +23,7 @@
 FunctionPtr NativeCallbackFunction(const FunctionType& c_signature,
                                    const Function& dart_target,
                                    const Instance& exceptional_return,
-                                   FfiFunctionKind kind);
+                                   FfiCallbackKind kind);
 
 // Builds a mapping from `callback-id` to code object / ...
 //
diff --git a/runtime/vm/compiler/frontend/base_flow_graph_builder.cc b/runtime/vm/compiler/frontend/base_flow_graph_builder.cc
index 59c2d68..8b78359 100644
--- a/runtime/vm/compiler/frontend/base_flow_graph_builder.cc
+++ b/runtime/vm/compiler/frontend/base_flow_graph_builder.cc
@@ -7,7 +7,6 @@
 #include <utility>
 
 #include "vm/compiler/backend/range_analysis.h"  // For Range.
-#include "vm/compiler/ffi/call.h"
 #include "vm/compiler/frontend/flow_graph_builder.h"  // For InlineExitCollector.
 #include "vm/compiler/jit/compiler.h"  // For Compiler::IsBackgroundCompilation().
 #include "vm/compiler/runtime_api.h"
@@ -1025,65 +1024,6 @@
   return Fragment(box);
 }
 
-Fragment BaseFlowGraphBuilder::BuildFfiAsFunctionInternalCall(
-    const TypeArguments& signatures,
-    bool is_leaf) {
-  ASSERT(signatures.Length() == 2);
-  const auto& sig0 = AbstractType::Handle(signatures.TypeAt(0));
-  const auto& sig1 = AbstractType::Handle(signatures.TypeAt(1));
-
-  if (!signatures.IsInstantiated() || !sig0.IsFunctionType() ||
-      !sig1.IsFunctionType()) {
-    const auto& msg = String::Handle(String::NewFormatted(
-        "Invalid type arguments passed to dart:ffi _asFunctionInternal: %s",
-        String::Handle(signatures.UserVisibleName()).ToCString()));
-    const auto& language_error =
-        Error::Handle(LanguageError::New(msg, Report::kError, Heap::kOld));
-    Report::LongJump(language_error);
-  }
-
-  const auto& dart_type = FunctionType::Cast(sig0);
-  const auto& native_type = FunctionType::Cast(sig1);
-
-  // AbiSpecificTypes can have an incomplete mapping.
-  const char* error = nullptr;
-  compiler::ffi::NativeFunctionTypeFromFunctionType(zone_, native_type, &error);
-  if (error != nullptr) {
-    const auto& language_error = Error::Handle(
-        LanguageError::New(String::Handle(String::New(error, Heap::kOld)),
-                           Report::kError, Heap::kOld));
-    Report::LongJump(language_error);
-  }
-
-  const auto& name =
-      String::Handle(parsed_function_->function().UserVisibleName());
-  const Function& target = Function::ZoneHandle(
-      compiler::ffi::TrampolineFunction(dart_type, native_type, is_leaf, name));
-
-  Fragment code;
-  // Store the pointer in the context, we cannot load the untagged address
-  // here as these can be unoptimized call sites.
-  LocalVariable* pointer = MakeTemporary();
-
-  code += Constant(target);
-
-  auto& context_slots = CompilerState::Current().GetDummyContextSlots(
-      /*context_id=*/0, /*num_variables=*/1);
-  code += AllocateContext(context_slots);
-  LocalVariable* context = MakeTemporary();
-
-  code += LoadLocal(context);
-  code += LoadLocal(pointer);
-  code += StoreNativeField(*context_slots[0]);
-
-  code += AllocateClosure();
-
-  // Drop address.
-  code += DropTempsPreserveTop(1);
-
-  return code;
-}
-
 Fragment BaseFlowGraphBuilder::DebugStepCheck(TokenPosition position) {
 #ifdef PRODUCT
   return Fragment();
diff --git a/runtime/vm/compiler/frontend/base_flow_graph_builder.h b/runtime/vm/compiler/frontend/base_flow_graph_builder.h
index ab8c026..e3df988 100644
--- a/runtime/vm/compiler/frontend/base_flow_graph_builder.h
+++ b/runtime/vm/compiler/frontend/base_flow_graph_builder.h
@@ -405,12 +405,6 @@
     return stack_ == nullptr ? 0 : stack_->definition()->temp_index() + 1;
   }
 
-  // Builds the graph for an invocation of '_asFunctionInternal'.
-  //
-  // 'signatures' contains the pair [<dart signature>, <native signature>].
-  Fragment BuildFfiAsFunctionInternalCall(const TypeArguments& signatures,
-                                          bool is_leaf);
-
   Fragment AllocateObject(TokenPosition position,
                           const Class& klass,
                           intptr_t argument_count);
diff --git a/runtime/vm/compiler/frontend/kernel_binary_flowgraph.cc b/runtime/vm/compiler/frontend/kernel_binary_flowgraph.cc
index 78984ab..ee5c2be 100644
--- a/runtime/vm/compiler/frontend/kernel_binary_flowgraph.cc
+++ b/runtime/vm/compiler/frontend/kernel_binary_flowgraph.cc
@@ -3348,18 +3348,18 @@
       return BuildNativeEffect();
     case MethodRecognizer::kReachabilityFence:
       return BuildReachabilityFence();
-    case MethodRecognizer::kFfiAsFunctionInternal:
-      return BuildFfiAsFunctionInternal();
+    case MethodRecognizer::kFfiCall:
+      return BuildFfiCall();
     case MethodRecognizer::kFfiNativeCallbackFunction:
       return BuildFfiNativeCallbackFunction(
-          FfiFunctionKind::kIsolateLocalStaticCallback);
+          FfiCallbackKind::kIsolateLocalStaticCallback);
     case MethodRecognizer::kFfiNativeAddressOf:
       return BuildFfiNativeAddressOf();
     case MethodRecognizer::kFfiNativeIsolateLocalCallbackFunction:
       return BuildFfiNativeCallbackFunction(
-          FfiFunctionKind::kIsolateLocalClosureCallback);
+          FfiCallbackKind::kIsolateLocalClosureCallback);
     case MethodRecognizer::kFfiNativeAsyncCallbackFunction:
-      return BuildFfiNativeCallbackFunction(FfiFunctionKind::kAsyncCallback);
+      return BuildFfiNativeCallbackFunction(FfiCallbackKind::kAsyncCallback);
     case MethodRecognizer::kFfiLoadAbiSpecificInt:
       return BuildLoadAbiSpecificInt(/*at_index=*/false);
     case MethodRecognizer::kFfiLoadAbiSpecificIntAtIndex:
@@ -6221,34 +6221,46 @@
   return code;
 }
 
-Fragment StreamingFlowGraphBuilder::BuildFfiAsFunctionInternal() {
+Fragment StreamingFlowGraphBuilder::BuildFfiCall() {
   const intptr_t argc = ReadUInt();               // Read argument count.
-  ASSERT(argc == 2);                              // Pointer, isLeaf.
+  ASSERT(argc == 1);                              // Target pointer.
   const intptr_t list_length = ReadListLength();  // Read types list length.
-  ASSERT(list_length == 2);  // Dart signature, then native signature
-  // Read types.
-  const TypeArguments& type_arguments = T.BuildTypeArguments(list_length);
-  Fragment code;
+  T.BuildTypeArguments(list_length);              // Read types.
   // Read positional argument count.
   const intptr_t positional_count = ReadListLength();
-  ASSERT(positional_count == 2);
-  code += BuildExpression();  // Build first positional argument (pointer).
+  ASSERT(positional_count == argc);
 
-  // The second argument, `isLeaf`, is only used internally and dictates whether
-  // we can do a lightweight leaf function call.
-  bool is_leaf = false;
-  Fragment frag = BuildExpression();
-  ASSERT(frag.entry->IsConstant());
-  if (frag.entry->AsConstant()->value().ptr() == Object::bool_true().ptr()) {
-    is_leaf = true;
-  }
-  Pop();
+  Fragment code;
+  // Push the target function pointer passed as Pointer object.
+  code += BuildExpression();
+  // This can only be Pointer, so it is always safe to LoadUntagged.
+  code += B->LoadUntagged(compiler::target::PointerBase::data_offset());
+  code += B->ConvertUntaggedToUnboxed(kUnboxedFfiIntPtr);
 
   // Skip (empty) named arguments list.
   const intptr_t named_args_len = ReadListLength();
   ASSERT(named_args_len == 0);
 
-  code += B->BuildFfiAsFunctionInternalCall(type_arguments, is_leaf);
+  const auto& native_type = FunctionType::ZoneHandle(
+      Z, parsed_function()->function().FfiCSignature());
+
+  // AbiSpecificTypes can have an incomplete mapping.
+  const char* error = nullptr;
+  compiler::ffi::NativeFunctionTypeFromFunctionType(Z, native_type, &error);
+  if (error != nullptr) {
+    const auto& language_error = Error::Handle(
+        LanguageError::New(String::Handle(String::New(error, Heap::kOld)),
+                           Report::kError, Heap::kOld));
+    Report::LongJump(language_error);
+  }
+
+  code += B->FfiCallFunctionBody(parsed_function()->function(), native_type,
+                                 /*first_argument_parameter_offset=*/1);
+
+  ASSERT(code.is_closed());
+
+  NullConstant();  // Maintain stack balance.
+
   return code;
 }
 
@@ -6313,23 +6325,23 @@
 }
 
 Fragment StreamingFlowGraphBuilder::BuildFfiNativeCallbackFunction(
-    FfiFunctionKind kind) {
+    FfiCallbackKind kind) {
   // The call-site must look like this (guaranteed by the FE which inserts it):
   //
-  // FfiFunctionKind::kIsolateLocalStaticCallback:
+  // FfiCallbackKind::kIsolateLocalStaticCallback:
   //   _nativeCallbackFunction<NativeSignatureType>(target, exceptionalReturn)
   //
-  // FfiFunctionKind::kAsyncCallback:
+  // FfiCallbackKind::kAsyncCallback:
   //   _nativeAsyncCallbackFunction<NativeSignatureType>()
   //
-  // FfiFunctionKind::kIsolateLocalClosureCallback:
+  // FfiCallbackKind::kIsolateLocalClosureCallback:
   //   _nativeIsolateLocalCallbackFunction<NativeSignatureType>(
   //       exceptionalReturn)
   //
   // The FE also guarantees that the arguments are constants.
 
-  const bool has_target = kind == FfiFunctionKind::kIsolateLocalStaticCallback;
-  const bool has_exceptional_return = kind != FfiFunctionKind::kAsyncCallback;
+  const bool has_target = kind == FfiCallbackKind::kIsolateLocalStaticCallback;
+  const bool has_exceptional_return = kind != FfiCallbackKind::kAsyncCallback;
   const intptr_t expected_argc =
       static_cast<int>(has_target) + static_cast<int>(has_exceptional_return);
 
diff --git a/runtime/vm/compiler/frontend/kernel_binary_flowgraph.h b/runtime/vm/compiler/frontend/kernel_binary_flowgraph.h
index e05f6e3..475d660 100644
--- a/runtime/vm/compiler/frontend/kernel_binary_flowgraph.h
+++ b/runtime/vm/compiler/frontend/kernel_binary_flowgraph.h
@@ -387,13 +387,12 @@
   Fragment BuildLoadAbiSpecificInt(bool at_index);
   Fragment BuildStoreAbiSpecificInt(bool at_index);
 
-  // Build FG for '_asFunctionInternal'. Reads an Arguments from the
-  // Kernel buffer and pushes the resulting closure.
-  Fragment BuildFfiAsFunctionInternal();
+  // Build FG for FFI call.
+  Fragment BuildFfiCall();
 
   // Build FG for '_nativeCallbackFunction'. Reads an Arguments from the
   // Kernel buffer and pushes the resulting Function object.
-  Fragment BuildFfiNativeCallbackFunction(FfiFunctionKind kind);
+  Fragment BuildFfiNativeCallbackFunction(FfiCallbackKind kind);
 
   Fragment BuildFfiNativeAddressOf();
 
diff --git a/runtime/vm/compiler/frontend/kernel_to_il.cc b/runtime/vm/compiler/frontend/kernel_to_il.cc
index 903781e..c360281 100644
--- a/runtime/vm/compiler/frontend/kernel_to_il.cc
+++ b/runtime/vm/compiler/frontend/kernel_to_il.cc
@@ -400,11 +400,12 @@
 }
 
 Fragment FlowGraphBuilder::FfiCall(
-    const compiler::ffi::CallMarshaller& marshaller) {
+    const compiler::ffi::CallMarshaller& marshaller,
+    bool is_leaf) {
   Fragment body;
 
-  FfiCallInstr* const call = new (Z) FfiCallInstr(
-      GetNextDeoptId(), marshaller, parsed_function_->function().FfiIsLeaf());
+  FfiCallInstr* const call =
+      new (Z) FfiCallInstr(GetNextDeoptId(), marshaller, is_leaf);
 
   for (intptr_t i = call->InputCount() - 1; i >= 0; --i) {
     call->SetInputAt(i, Pop());
@@ -5030,14 +5031,12 @@
 
 FlowGraph* FlowGraphBuilder::BuildGraphOfFfiTrampoline(
     const Function& function) {
-  switch (function.GetFfiFunctionKind()) {
-    case FfiFunctionKind::kIsolateLocalStaticCallback:
-    case FfiFunctionKind::kIsolateLocalClosureCallback:
+  switch (function.GetFfiCallbackKind()) {
+    case FfiCallbackKind::kIsolateLocalStaticCallback:
+    case FfiCallbackKind::kIsolateLocalClosureCallback:
       return BuildGraphOfSyncFfiCallback(function);
-    case FfiFunctionKind::kAsyncCallback:
+    case FfiCallbackKind::kAsyncCallback:
       return BuildGraphOfAsyncFfiCallback(function);
-    case FfiFunctionKind::kCall:
-      return BuildGraphOfFfiCall(function);
   }
   UNREACHABLE();
   return nullptr;
@@ -5128,26 +5127,6 @@
   return FfiNativeLookupAddress(native_instance);
 }
 
-Fragment FlowGraphBuilder::FfiCallLookupAddress(const Function& function) {
-  ASSERT(function.IsFfiTrampoline());
-  const intptr_t kClosureParameterOffset = 0;
-  Fragment body;
-  // Push the function pointer, which is stored (as Pointer object) in the
-  // first slot of the context.
-  body +=
-      LoadLocal(parsed_function_->ParameterVariable(kClosureParameterOffset));
-  body += LoadNativeField(Slot::Closure_context());
-  body += LoadNativeField(Slot::GetContextVariableSlotFor(
-      thread_, *MakeImplicitClosureScope(
-                    Z, Class::Handle(IG->object_store()->ffi_pointer_class()))
-                    ->context_variables()[0]));
-
-  // This can only be Pointer, so it is always safe to LoadUntagged.
-  body += LoadUntagged(compiler::target::PointerBase::data_offset());
-  body += ConvertUntaggedToUnboxed(kUnboxedFfiIntPtr);
-  return body;
-}
-
 Fragment FlowGraphBuilder::FfiNativeFunctionBody(const Function& function) {
   ASSERT(function.is_ffi_native());
   ASSERT(!IsRecognizedMethodForFlowGraph(function));
@@ -5157,18 +5136,16 @@
 
   Fragment body;
   body += FfiNativeLookupAddress(function);
-  body += FfiCallFunctionBody(function, c_signature);
+  body += FfiCallFunctionBody(function, c_signature,
+                              /*first_argument_parameter_offset=*/0);
   return body;
 }
 
 Fragment FlowGraphBuilder::FfiCallFunctionBody(
     const Function& function,
-    const FunctionType& c_signature) {
-  ASSERT(function.is_ffi_native() || function.IsFfiTrampoline());
-  const bool is_ffi_native = function.is_ffi_native();
-  const intptr_t kClosureParameterOffset = 0;
-  const intptr_t first_argument_parameter_offset =
-      is_ffi_native ? 0 : kClosureParameterOffset + 1;
+    const FunctionType& c_signature,
+    intptr_t first_argument_parameter_offset) {
+  ASSERT(function.is_ffi_native() || function.IsFfiCallClosure());
 
   LocalVariable* address = MakeTemporary("address");
 
@@ -5258,7 +5235,7 @@
     body += LoadLocal(return_compound_typed_data);
   }
 
-  body += FfiCall(marshaller);
+  body += FfiCall(marshaller, function.FfiIsLeaf());
 
   for (intptr_t i = 0; i < marshaller.num_args(); i++) {
     if (marshaller.IsPointer(i)) {
@@ -5321,31 +5298,6 @@
   return body;
 }
 
-FlowGraph* FlowGraphBuilder::BuildGraphOfFfiCall(const Function& function) {
-  graph_entry_ =
-      new (Z) GraphEntryInstr(*parsed_function_, Compiler::kNoOSRDeoptId);
-
-  auto normal_entry = BuildFunctionEntry(graph_entry_);
-  graph_entry_->set_normal_entry(normal_entry);
-
-  PrologueInfo prologue_info(-1, -1);
-
-  BlockEntryInstr* instruction_cursor =
-      BuildPrologue(normal_entry, &prologue_info);
-
-  Fragment function_body(instruction_cursor);
-  function_body += CheckStackOverflowInPrologue(function.token_pos());
-
-  const auto& c_signature =
-      FunctionType::ZoneHandle(Z, function.FfiCSignature());
-
-  function_body += FfiCallLookupAddress(function);
-  function_body += FfiCallFunctionBody(function, c_signature);
-
-  return new (Z) FlowGraph(*parsed_function_, graph_entry_, last_used_block_id_,
-                           prologue_info);
-}
-
 Fragment FlowGraphBuilder::LoadNativeArg(
     const compiler::ffi::CallbackMarshaller& marshaller,
     intptr_t arg_index) {
@@ -5381,8 +5333,8 @@
   RELEASE_ASSERT(error == nullptr);
   RELEASE_ASSERT(marshaller_ptr != nullptr);
   const auto& marshaller = *marshaller_ptr;
-  const bool is_closure = function.GetFfiFunctionKind() ==
-                          FfiFunctionKind::kIsolateLocalClosureCallback;
+  const bool is_closure = function.GetFfiCallbackKind() ==
+                          FfiCallbackKind::kIsolateLocalClosureCallback;
 
   graph_entry_ =
       new (Z) GraphEntryInstr(*parsed_function_, Compiler::kNoOSRDeoptId);
diff --git a/runtime/vm/compiler/frontend/kernel_to_il.h b/runtime/vm/compiler/frontend/kernel_to_il.h
index 88fa285..c112a32 100644
--- a/runtime/vm/compiler/frontend/kernel_to_il.h
+++ b/runtime/vm/compiler/frontend/kernel_to_il.h
@@ -137,8 +137,6 @@
   FlowGraph* BuildGraphOfFfiTrampoline(const Function& function);
   FlowGraph* BuildGraphOfSyncFfiCallback(const Function& function);
   FlowGraph* BuildGraphOfAsyncFfiCallback(const Function& function);
-  FlowGraph* BuildGraphOfFfiCall(const Function& function);
-  Fragment FfiCallLookupAddress(const Function& function);
 
   // Resolves the address of a native symbol from the constant data of a
   // vm:ffi:native pragma.
@@ -150,7 +148,8 @@
   Fragment FfiNativeLookupAddress(const Function& function);
   // Expects target address on stack.
   Fragment FfiCallFunctionBody(const Function& function,
-                               const FunctionType& c_signature);
+                               const FunctionType& c_signature,
+                               intptr_t first_argument_parameter_offset);
   Fragment FfiNativeFunctionBody(const Function& function);
   Fragment NativeFunctionBody(const Function& function,
                               LocalVariable* first_parameter);
@@ -204,7 +203,8 @@
       bool receiver_is_not_smi = false,
       bool is_call_on_this = false);
 
-  Fragment FfiCall(const compiler::ffi::CallMarshaller& marshaller);
+  Fragment FfiCall(const compiler::ffi::CallMarshaller& marshaller,
+                   bool is_leaf);
 
   Fragment CCall(
       const compiler::ffi::NativeCallingConvention& native_calling_convention);
diff --git a/runtime/vm/compiler/frontend/scope_builder.cc b/runtime/vm/compiler/frontend/scope_builder.cc
index 328eae0..435d92f 100644
--- a/runtime/vm/compiler/frontend/scope_builder.cc
+++ b/runtime/vm/compiler/frontend/scope_builder.cc
@@ -154,8 +154,7 @@
           FunctionNodeHelper::kPositionalParameters);
       // NOTE: FunctionNode is read further below the if.
 
-      intptr_t pos = 0;
-      if (function.is_ffi_native()) {
+      if (function.is_ffi_native() || function.IsFfiCallClosure()) {
         needs_expr_temp_ = true;
         // Calls with handles need try/catch variables.
         if (function.FfiCSignatureContainsHandles()) {
@@ -167,7 +166,9 @@
           FinalizeCatchVariables();
           --depth_.catch_;
         }
-      } else if (function.IsClosureFunction()) {
+      }
+      intptr_t pos = 0;
+      if (function.IsClosureFunction()) {
         LocalVariable* closure_parameter = MakeVariable(
             TokenPosition::kNoSource, TokenPosition::kNoSource,
             Symbols::ClosureParameter(), AbstractType::dynamic_type());
@@ -406,17 +407,14 @@
     }
     case UntaggedFunction::kFfiTrampoline: {
       needs_expr_temp_ = true;
-      // Callbacks and calls with handles need try/catch variables.
-      if ((function.GetFfiFunctionKind() != FfiFunctionKind::kCall ||
-           function.FfiCSignatureContainsHandles())) {
-        ++depth_.try_;
-        AddTryVariables();
-        --depth_.try_;
-        ++depth_.catch_;
-        AddCatchVariables();
-        FinalizeCatchVariables();
-        --depth_.catch_;
-      }
+      // Callbacks need try/catch variables.
+      ++depth_.try_;
+      AddTryVariables();
+      --depth_.try_;
+      ++depth_.catch_;
+      AddCatchVariables();
+      FinalizeCatchVariables();
+      --depth_.catch_;
       FALL_THROUGH;
     }
     case UntaggedFunction::kInvokeFieldDispatcher: {
@@ -437,7 +435,7 @@
         LocalVariable* variable = MakeVariable(
             TokenPosition::kNoSource, TokenPosition::kNoSource,
             String::ZoneHandle(Z, function.ParameterNameAt(i)),
-            AbstractType::ZoneHandle(Z, function.IsFfiTrampoline()
+            AbstractType::ZoneHandle(Z, function.IsFfiCallbackTrampoline()
                                             ? function.ParameterTypeAt(i)
                                             : Object::dynamic_type().ptr()));
         bool added = scope_->InsertParameterAt(i, variable);
diff --git a/runtime/vm/compiler/jit/compiler.cc b/runtime/vm/compiler/jit/compiler.cc
index ff2122f..46b07a2 100644
--- a/runtime/vm/compiler/jit/compiler.cc
+++ b/runtime/vm/compiler/jit/compiler.cc
@@ -476,8 +476,7 @@
     }
   }
 
-  if (function.IsFfiTrampoline() &&
-      function.GetFfiFunctionKind() != FfiFunctionKind::kCall) {
+  if (function.IsFfiCallbackTrampoline()) {
     compiler::ffi::SetFfiCallbackCode(thread(), function, code);
   }
 
diff --git a/runtime/vm/compiler/recognized_methods_list.h b/runtime/vm/compiler/recognized_methods_list.h
index 3f0dcc4..8b47dc3 100644
--- a/runtime/vm/compiler/recognized_methods_list.h
+++ b/runtime/vm/compiler/recognized_methods_list.h
@@ -272,7 +272,7 @@
   V(_WeakReference, get:target, WeakReference_getTarget, 0xc98185aa)           \
   V(_WeakReference, set:_target, WeakReference_setTarget, 0xc71add9a)          \
   V(::, _abi, FfiAbi, 0x7c3c2b95)                                              \
-  V(::, _asFunctionInternal, FfiAsFunctionInternal, 0x630c8491)                \
+  V(::, _ffiCall, FfiCall, 0x6118e962)                                         \
   V(::, _nativeCallbackFunction, FfiNativeCallbackFunction, 0x3fe722bc)        \
   V(::, _nativeAsyncCallbackFunction, FfiNativeAsyncCallbackFunction,          \
     0xbec4b7b9)                                                                \
diff --git a/runtime/vm/ffi_callback_metadata.cc b/runtime/vm/ffi_callback_metadata.cc
index c8d5f3c..93febd1 100644
--- a/runtime/vm/ffi_callback_metadata.cc
+++ b/runtime/vm/ffi_callback_metadata.cc
@@ -275,13 +275,13 @@
   if (closure.IsNull()) {
     // If the closure is null, it means the target is a static function, so is
     // baked into the trampoline and is an ordinary sync callback.
-    ASSERT(function.GetFfiFunctionKind() ==
-           FfiFunctionKind::kIsolateLocalStaticCallback);
+    ASSERT(function.GetFfiCallbackKind() ==
+           FfiCallbackKind::kIsolateLocalStaticCallback);
     return CreateSyncFfiCallbackImpl(isolate, zone, function, nullptr,
                                      list_head);
   } else {
-    ASSERT(function.GetFfiFunctionKind() ==
-           FfiFunctionKind::kIsolateLocalClosureCallback);
+    ASSERT(function.GetFfiCallbackKind() ==
+           FfiCallbackKind::kIsolateLocalClosureCallback);
     return CreateSyncFfiCallbackImpl(isolate, zone, function,
                                      CreatePersistentHandle(isolate, closure),
                                      list_head);
@@ -319,7 +319,7 @@
     const Function& send_function,
     Dart_Port send_port,
     Metadata** list_head) {
-  ASSERT(send_function.GetFfiFunctionKind() == FfiFunctionKind::kAsyncCallback);
+  ASSERT(send_function.GetFfiCallbackKind() == FfiCallbackKind::kAsyncCallback);
   return CreateMetadataEntry(isolate, TrampolineType::kAsync,
                              GetEntryPoint(zone, send_function),
                              static_cast<uint64_t>(send_port), list_head);
diff --git a/runtime/vm/ffi_callback_metadata_test.cc b/runtime/vm/ffi_callback_metadata_test.cc
index 2040e1d..b09896b 100644
--- a/runtime/vm/ffi_callback_metadata_test.cc
+++ b/runtime/vm/ffi_callback_metadata_test.cc
@@ -22,7 +22,7 @@
 
 namespace dart {
 
-FunctionPtr CreateTestFunction(FfiFunctionKind kind) {
+FunctionPtr CreateTestFunction(FfiCallbackKind kind) {
   const auto& ffi_lib = Library::Handle(Library::FfiLibrary());
   const auto& ffi_void = Class::Handle(ffi_lib.LookupClass(Symbols::FfiVoid()));
   const auto& ffi_void_type =
@@ -92,7 +92,7 @@
     auto* zone = thread->zone();
 
     const auto& func = Function::Handle(
-        CreateTestFunction(FfiFunctionKind::kIsolateLocalStaticCallback));
+        CreateTestFunction(FfiCallbackKind::kIsolateLocalStaticCallback));
     const auto& code = Code::Handle(func.EnsureHasCode());
     EXPECT(!code.IsNull());
 
@@ -185,7 +185,7 @@
     auto* zone = thread->zone();
 
     const Function& func =
-        Function::Handle(CreateTestFunction(FfiFunctionKind::kAsyncCallback));
+        Function::Handle(CreateTestFunction(FfiCallbackKind::kAsyncCallback));
     const Code& code = Code::Handle(func.EnsureHasCode());
     EXPECT(!code.IsNull());
 
@@ -280,13 +280,13 @@
     auto* zone = thread->zone();
 
     const Function& func = Function::Handle(
-        CreateTestFunction(FfiFunctionKind::kIsolateLocalClosureCallback));
+        CreateTestFunction(FfiCallbackKind::kIsolateLocalClosureCallback));
     const Code& code = Code::Handle(func.EnsureHasCode());
     EXPECT(!code.IsNull());
 
-    // Using a FfiFunctionKind::kSync function as a dummy closure.
+    // Using a FfiCallbackKind::kSync function as a dummy closure.
     const Function& closure_func = Function::Handle(
-        CreateTestFunction(FfiFunctionKind::kIsolateLocalStaticCallback));
+        CreateTestFunction(FfiCallbackKind::kIsolateLocalStaticCallback));
     const Context& context = Context::Handle(Context::null());
     const Closure& closure1 = Closure::Handle(
         Closure::New(Object::null_type_arguments(),
@@ -373,7 +373,7 @@
   auto* fcm = FfiCallbackMetadata::Instance();
 
   const Function& func =
-      Function::Handle(CreateTestFunction(FfiFunctionKind::kAsyncCallback));
+      Function::Handle(CreateTestFunction(FfiCallbackKind::kAsyncCallback));
   const Code& code = Code::Handle(func.EnsureHasCode());
   EXPECT(!code.IsNull());
 
@@ -440,7 +440,7 @@
   FfiCallbackMetadata::Metadata* list_head = nullptr;
 
   const auto& sync_func = Function::Handle(
-      CreateTestFunction(FfiFunctionKind::kIsolateLocalStaticCallback));
+      CreateTestFunction(FfiCallbackKind::kIsolateLocalStaticCallback));
   const auto& sync_code = Code::Handle(sync_func.EnsureHasCode());
   EXPECT(!sync_code.IsNull());
 
@@ -521,11 +521,11 @@
   FfiCallbackMetadata::Metadata* list_head = nullptr;
 
   const Function& async_func =
-      Function::Handle(CreateTestFunction(FfiFunctionKind::kAsyncCallback));
+      Function::Handle(CreateTestFunction(FfiCallbackKind::kAsyncCallback));
   const Code& async_code = Code::Handle(async_func.EnsureHasCode());
   EXPECT(!async_code.IsNull());
   const Function& sync_func = Function::Handle(
-      CreateTestFunction(FfiFunctionKind::kIsolateLocalStaticCallback));
+      CreateTestFunction(FfiCallbackKind::kIsolateLocalStaticCallback));
   const auto& sync_code = Code::Handle(sync_func.EnsureHasCode());
   EXPECT(!sync_code.IsNull());
 
diff --git a/runtime/vm/object.cc b/runtime/vm/object.cc
index 1fd399b..ae81ed8 100644
--- a/runtime/vm/object.cc
+++ b/runtime/vm/object.cc
@@ -8337,7 +8337,8 @@
 
 FunctionPtr Function::implicit_closure_function() const {
   if (IsClosureFunction() || IsDispatcherOrImplicitAccessor() ||
-      IsFieldInitializer() || IsFfiTrampoline() || IsMethodExtractor()) {
+      IsFieldInitializer() || IsFfiCallbackTrampoline() ||
+      IsMethodExtractor()) {
     return Function::null();
   }
   const Object& obj = Object::Handle(data());
@@ -8372,7 +8373,7 @@
 }
 
 void Function::SetFfiCSignature(const FunctionType& sig) const {
-  ASSERT(IsFfiTrampoline());
+  ASSERT(IsFfiCallbackTrampoline());
   const Object& obj = Object::Handle(data());
   ASSERT(!obj.IsNull());
   FfiTrampolineData::Cast(obj).set_c_signature(sig);
@@ -8380,15 +8381,21 @@
 
 FunctionTypePtr Function::FfiCSignature() const {
   auto* const zone = Thread::Current()->zone();
-  if (IsFfiTrampoline()) {
+  if (IsFfiCallbackTrampoline()) {
     const Object& obj = Object::Handle(zone, data());
     ASSERT(!obj.IsNull());
     return FfiTrampolineData::Cast(obj).c_signature();
   }
-  ASSERT(is_ffi_native());
-  auto const& native_instance = Instance::Handle(GetNativeAnnotation());
+  auto& pragma_value = Instance::Handle(zone);
+  if (is_ffi_native()) {
+    pragma_value = GetNativeAnnotation();
+  } else if (IsFfiCallClosure()) {
+    pragma_value = GetFfiCallClosurePragmaValue();
+  } else {
+    UNREACHABLE();
+  }
   const auto& type_args =
-      TypeArguments::Handle(zone, native_instance.GetTypeArguments());
+      TypeArguments::Handle(zone, pragma_value.GetTypeArguments());
   ASSERT(type_args.Length() == 1);
   const auto& native_type =
       FunctionType::Cast(AbstractType::ZoneHandle(zone, type_args.TypeAt(0)));
@@ -8415,7 +8422,7 @@
 
 // Keep consistent with BaseMarshaller::IsCompound.
 bool Function::FfiCSignatureReturnsStruct() const {
-  ASSERT(IsFfiTrampoline());
+  ASSERT(IsFfiCallbackTrampoline());
   Zone* zone = Thread::Current()->zone();
   const auto& c_signature = FunctionType::Handle(zone, FfiCSignature());
   const auto& type = AbstractType::Handle(zone, c_signature.result_type());
@@ -8441,8 +8448,7 @@
 }
 
 int32_t Function::FfiCallbackId() const {
-  ASSERT(IsFfiTrampoline());
-  ASSERT(GetFfiFunctionKind() != FfiFunctionKind::kCall);
+  ASSERT(IsFfiCallbackTrampoline());
 
   const auto& obj = Object::Handle(data());
   ASSERT(!obj.IsNull());
@@ -8454,8 +8460,7 @@
 }
 
 void Function::AssignFfiCallbackId(int32_t callback_id) const {
-  ASSERT(IsFfiTrampoline());
-  ASSERT(GetFfiFunctionKind() != FfiFunctionKind::kCall);
+  ASSERT(IsFfiCallbackTrampoline());
 
   const auto& obj = Object::Handle(data());
   ASSERT(!obj.IsNull());
@@ -8466,69 +8471,64 @@
 }
 
 bool Function::FfiIsLeaf() const {
-  if (IsFfiTrampoline()) {
-    const Object& obj = Object::Handle(untag()->data());
-    ASSERT(!obj.IsNull());
-    return FfiTrampolineData::Cast(obj).is_leaf();
-  }
-  ASSERT(is_ffi_native());
   Zone* zone = Thread::Current()->zone();
-  auto const& native_instance = Instance::Handle(GetNativeAnnotation());
-  const auto& native_class = Class::Handle(zone, native_instance.clazz());
-  const auto& native_class_fields = Array::Handle(zone, native_class.fields());
-  ASSERT(native_class_fields.Length() == 4);
-  const auto& is_leaf_field =
-      Field::Handle(zone, Field::RawCast(native_class_fields.At(3)));
-  ASSERT(!is_leaf_field.is_static());
-  return Bool::Handle(zone,
-                      Bool::RawCast(native_instance.GetField(is_leaf_field)))
+  auto& pragma_value = Instance::Handle(zone);
+  if (is_ffi_native()) {
+    pragma_value = GetNativeAnnotation();
+  } else if (IsFfiCallClosure()) {
+    pragma_value = GetFfiCallClosurePragmaValue();
+  } else {
+    UNREACHABLE();
+  }
+  const auto& pragma_value_class = Class::Handle(zone, pragma_value.clazz());
+  const auto& pragma_value_fields =
+      Array::Handle(zone, pragma_value_class.fields());
+  ASSERT(pragma_value_fields.Length() >= 1);
+  const auto& is_leaf_field = Field::Handle(
+      zone,
+      Field::RawCast(pragma_value_fields.At(pragma_value_fields.Length() - 1)));
+  ASSERT(is_leaf_field.name() == Symbols::isLeaf().ptr());
+  return Bool::Handle(zone, Bool::RawCast(pragma_value.GetField(is_leaf_field)))
       .value();
 }
 
-void Function::SetFfiIsLeaf(bool is_leaf) const {
-  ASSERT(IsFfiTrampoline());
-  const Object& obj = Object::Handle(untag()->data());
-  ASSERT(!obj.IsNull());
-  FfiTrampolineData::Cast(obj).set_is_leaf(is_leaf);
-}
-
 FunctionPtr Function::FfiCallbackTarget() const {
-  ASSERT(IsFfiTrampoline());
+  ASSERT(IsFfiCallbackTrampoline());
   const Object& obj = Object::Handle(data());
   ASSERT(!obj.IsNull());
   return FfiTrampolineData::Cast(obj).callback_target();
 }
 
 void Function::SetFfiCallbackTarget(const Function& target) const {
-  ASSERT(IsFfiTrampoline());
+  ASSERT(IsFfiCallbackTrampoline());
   const Object& obj = Object::Handle(data());
   ASSERT(!obj.IsNull());
   FfiTrampolineData::Cast(obj).set_callback_target(target);
 }
 
 InstancePtr Function::FfiCallbackExceptionalReturn() const {
-  ASSERT(IsFfiTrampoline());
+  ASSERT(IsFfiCallbackTrampoline());
   const Object& obj = Object::Handle(data());
   ASSERT(!obj.IsNull());
   return FfiTrampolineData::Cast(obj).callback_exceptional_return();
 }
 
 void Function::SetFfiCallbackExceptionalReturn(const Instance& value) const {
-  ASSERT(IsFfiTrampoline());
+  ASSERT(IsFfiCallbackTrampoline());
   const Object& obj = Object::Handle(data());
   ASSERT(!obj.IsNull());
   FfiTrampolineData::Cast(obj).set_callback_exceptional_return(value);
 }
 
-FfiFunctionKind Function::GetFfiFunctionKind() const {
-  ASSERT(IsFfiTrampoline());
+FfiCallbackKind Function::GetFfiCallbackKind() const {
+  ASSERT(IsFfiCallbackTrampoline());
   const Object& obj = Object::Handle(data());
   ASSERT(!obj.IsNull());
   return FfiTrampolineData::Cast(obj).ffi_function_kind();
 }
 
-void Function::SetFfiFunctionKind(FfiFunctionKind value) const {
-  ASSERT(IsFfiTrampoline());
+void Function::SetFfiCallbackKind(FfiCallbackKind value) const {
+  ASSERT(IsFfiCallbackTrampoline());
   const Object& obj = Object::Handle(data());
   ASSERT(!obj.IsNull());
   FfiTrampolineData::Cast(obj).set_ffi_function_kind(value);
@@ -9133,7 +9133,8 @@
 }
 
 bool Function::ForceOptimize() const {
-  if (RecognizedKindForceOptimize() || IsFfiTrampoline() || is_ffi_native() ||
+  if (RecognizedKindForceOptimize() || IsFfiCallClosure() ||
+      IsFfiCallbackTrampoline() || is_ffi_native() ||
       IsTypedDataViewFactory() || IsUnmodifiableTypedDataViewFactory()) {
     return true;
   }
@@ -9174,6 +9175,25 @@
   return InVmTests(*this);
 }
 
+bool Function::IsFfiCallClosure() const {
+  if (!IsNonImplicitClosureFunction()) return false;
+  if (!has_pragma()) return false;
+  return Library::FindPragma(Thread::Current(), /*only_core=*/false, *this,
+                             Symbols::vm_ffi_call_closure());
+}
+
+InstancePtr Function::GetFfiCallClosurePragmaValue() const {
+  ASSERT(IsFfiCallClosure());
+  Thread* thread = Thread::Current();
+  Zone* zone = thread->zone();
+  auto& pragma_value = Object::Handle(zone);
+  Library::FindPragma(thread, /*only_core=*/false, *this,
+                      Symbols::vm_ffi_call_closure(),
+                      /*multiple=*/false, &pragma_value);
+  ASSERT(!pragma_value.IsNull());
+  return Instance::Cast(pragma_value).ptr();
+}
+
 bool Function::RecognizedKindForceOptimize() const {
   switch (recognized_kind()) {
     // Uses unboxed/untagged data not supported in unoptimized.
@@ -9248,7 +9268,7 @@
 #if !defined(DART_PRECOMPILED_RUNTIME)
 bool Function::CanBeInlined() const {
   if (ForceOptimize()) {
-    if (IsFfiTrampoline() || is_ffi_native()) {
+    if (IsFfiCallClosure() || IsFfiCallbackTrampoline() || is_ffi_native()) {
       // We currently don't support inlining FFI trampolines. Some of them
       // are naturally non-inlinable because they contain a try/catch block,
       // but this condition is broader than strictly necessary.
@@ -11000,7 +11020,7 @@
 
 intptr_t Function::KernelLibraryIndex() const {
   if (IsNoSuchMethodDispatcher() || IsInvokeFieldDispatcher() ||
-      IsFfiTrampoline()) {
+      IsFfiCallbackTrampoline()) {
     return -1;
   }
   if (is_eval_function()) {
@@ -11763,16 +11783,12 @@
   StoreNonPointer(&untag()->callback_id_, callback_id);
 }
 
-void FfiTrampolineData::set_is_leaf(bool is_leaf) const {
-  StoreNonPointer(&untag()->is_leaf_, is_leaf);
-}
-
 void FfiTrampolineData::set_callback_exceptional_return(
     const Instance& value) const {
   untag()->set_callback_exceptional_return(value.ptr());
 }
 
-void FfiTrampolineData::set_ffi_function_kind(FfiFunctionKind kind) const {
+void FfiTrampolineData::set_ffi_function_kind(FfiCallbackKind kind) const {
   StoreNonPointer(&untag()->ffi_function_kind_, static_cast<uint8_t>(kind));
 }
 
diff --git a/runtime/vm/object.h b/runtime/vm/object.h
index 80ba527..255cab4 100644
--- a/runtime/vm/object.h
+++ b/runtime/vm/object.h
@@ -2949,8 +2949,7 @@
   }
 };
 
-enum class FfiFunctionKind : uint8_t {
-  kCall,
+enum class FfiCallbackKind : uint8_t {
   kIsolateLocalStaticCallback,
   kIsolateLocalClosureCallback,
   kAsyncCallback,
@@ -2986,37 +2985,31 @@
   bool FfiCSignatureReturnsStruct() const;
 
   // Can only be called on FFI trampolines.
-  // -1 for Dart -> native calls.
   int32_t FfiCallbackId() const;
 
   // Should be called when ffi trampoline function object is created.
   void AssignFfiCallbackId(int32_t callback_id) const;
 
-  // Can only be called on FFI trampolines.
+  // Can only be called on FFI natives and FFI call closures.
   bool FfiIsLeaf() const;
 
   // Can only be called on FFI trampolines.
-  void SetFfiIsLeaf(bool is_leaf) const;
-
-  // Can only be called on FFI trampolines.
-  // Null for Dart -> native calls.
   FunctionPtr FfiCallbackTarget() const;
 
   // Can only be called on FFI trampolines.
   void SetFfiCallbackTarget(const Function& target) const;
 
   // Can only be called on FFI trampolines.
-  // Null for Dart -> native calls.
   InstancePtr FfiCallbackExceptionalReturn() const;
 
   // Can only be called on FFI trampolines.
   void SetFfiCallbackExceptionalReturn(const Instance& value) const;
 
   // Can only be called on FFI trampolines.
-  FfiFunctionKind GetFfiFunctionKind() const;
+  FfiCallbackKind GetFfiCallbackKind() const;
 
   // Can only be called on FFI trampolines.
-  void SetFfiFunctionKind(FfiFunctionKind value) const;
+  void SetFfiCallbackKind(FfiCallbackKind value) const;
 
   // Return the signature of this function.
   PRECOMPILER_WSR_FIELD_DECLARATION(FunctionType, signature);
@@ -3902,15 +3895,22 @@
   }
 
   // Returns true if this function represents an ffi trampoline.
-  bool IsFfiTrampoline() const {
+  bool IsFfiCallbackTrampoline() const {
     return kind() == UntaggedFunction::kFfiTrampoline;
   }
-  static bool IsFfiTrampoline(FunctionPtr function) {
+  static bool IsFfiCallbackTrampoline(FunctionPtr function) {
     NoSafepointScope no_safepoint;
     return function->untag()->kind_tag_.Read<KindBits>() ==
            UntaggedFunction::kFfiTrampoline;
   }
 
+  // Returns true if this function is a closure function
+  // used to represent ffi call.
+  bool IsFfiCallClosure() const;
+
+  // Returns value of vm:ffi:call-closure pragma.
+  InstancePtr GetFfiCallClosurePragmaValue() const;
+
   // Returns true for functions which execution can be suspended
   // using Suspend/Resume stubs. Such functions have an artificial
   // :suspend_state local variable at the fixed location of the frame.
@@ -4347,17 +4347,14 @@
   }
   void set_callback_exceptional_return(const Instance& value) const;
 
-  FfiFunctionKind ffi_function_kind() const {
-    return static_cast<FfiFunctionKind>(untag()->ffi_function_kind_);
+  FfiCallbackKind ffi_function_kind() const {
+    return static_cast<FfiCallbackKind>(untag()->ffi_function_kind_);
   }
-  void set_ffi_function_kind(FfiFunctionKind kind) const;
+  void set_ffi_function_kind(FfiCallbackKind kind) const;
 
   int32_t callback_id() const { return untag()->callback_id_; }
   void set_callback_id(int32_t value) const;
 
-  bool is_leaf() const { return untag()->is_leaf_; }
-  void set_is_leaf(bool value) const;
-
   static FfiTrampolineDataPtr New();
 
   FINAL_HEAP_OBJECT_IMPLEMENTATION(FfiTrampolineData, Object);
diff --git a/runtime/vm/raw_object.h b/runtime/vm/raw_object.h
index 291d46e..15e8b7f 100644
--- a/runtime/vm/raw_object.h
+++ b/runtime/vm/raw_object.h
@@ -1501,10 +1501,7 @@
   // Check 'callback_target_' to determine if this is a callback or not.
   int32_t callback_id_;
 
-  // Whether this is a leaf call - i.e. one that doesn't call back into Dart.
-  bool is_leaf_;
-
-  // The kind of trampoline this is. See FfiFunctionKind.
+  // The kind of trampoline this is. See FfiCallbackKind.
   uint8_t ffi_function_kind_;
 };
 
diff --git a/runtime/vm/resolver.cc b/runtime/vm/resolver.cc
index 1d1b196..1947035 100644
--- a/runtime/vm/resolver.cc
+++ b/runtime/vm/resolver.cc
@@ -129,7 +129,7 @@
     // FfiTrampolines are the only functions that can still be called
     // dynamically without going through a dynamic invocation forwarder.
     RELEASE_ASSERT(!Function::IsDynamicInvocationForwarderName(function_name) &&
-                   !function.IsFfiTrampoline());
+                   !function.IsFfiCallbackTrampoline());
     // The signature for this function was dropped in the precompiler, which
     // means it is not a possible target for a dynamic call in the program.
     // That means we're resolving an UnlinkedCall for an InstanceCall to
diff --git a/runtime/vm/symbols.h b/runtime/vm/symbols.h
index 846d717..c937627 100644
--- a/runtime/vm/symbols.h
+++ b/runtime/vm/symbols.h
@@ -501,6 +501,7 @@
   V(from, "from")                                                              \
   V(get, "get")                                                                \
   V(index_temp, ":index_temp")                                                 \
+  V(isLeaf, "isLeaf")                                                          \
   V(isPaused, "isPaused")                                                      \
   V(match_end_index, ":match_end_index")                                       \
   V(match_start_index, ":match_start_index")                                   \
@@ -529,6 +530,7 @@
   V(vm_exact_result_type, "vm:exact-result-type")                              \
   V(vm_external_name, "vm:external-name")                                      \
   V(vm_ffi_abi_specific_mapping, "vm:ffi:abi-specific-mapping")                \
+  V(vm_ffi_call_closure, "vm:ffi:call-closure")                                \
   V(vm_ffi_native, "vm:ffi:native")                                            \
   V(vm_ffi_native_assets, "vm:ffi:native-assets")                              \
   V(vm_ffi_struct_fields, "vm:ffi:struct-fields")                              \
diff --git a/sdk/lib/_internal/vm/lib/ffi_patch.dart b/sdk/lib/_internal/vm/lib/ffi_patch.dart
index b1dd9ec..6c1e88f 100644
--- a/sdk/lib/_internal/vm/lib/ffi_patch.dart
+++ b/sdk/lib/_internal/vm/lib/ffi_patch.dart
@@ -82,13 +82,21 @@
 @pragma("vm:idempotent")
 external Pointer<T> _fromAddress<T extends NativeType>(int ptr);
 
-// The real implementation of this function (for interface calls) lives in
-// BuildFfiAsFunctionInternal in the Kernel frontend. No calls can actually
-// reach this function.
+/// Argument for vm:ffi:call-closure pragma describing FFI call.
+final class _FfiCall<NativeSignature> {
+  // Implementation note: VM hardcodes the layout of this class (number and
+  // order of its fields), so adding/removing/changing fields requires
+  // updating the VM code (see Function::GetFfiCallClosurePragmaValue()).
+  final bool isLeaf;
+  const _FfiCall({this.isLeaf = false});
+}
+
+// Helper function to perform FFI call.
+// Inserted by FFI kernel transformation into the FFI call closures.
+// Implemented in BuildFfiCall
+// in runtime/vm/compiler/frontend/kernel_binary_flowgraph.cc.
 @pragma("vm:recognized", "other")
-@pragma("vm:external-name", "Ffi_asFunctionInternal")
-external DS _asFunctionInternal<DS extends Function, NS extends Function>(
-    Pointer<NativeFunction<NS>> ptr, bool isLeaf);
+external ReturnType _ffiCall<ReturnType>(Pointer<NativeFunction> target);
 
 @pragma("vm:recognized", "other")
 @pragma("vm:idempotent")
diff --git a/sdk/lib/ffi/ffi.dart b/sdk/lib/ffi/ffi.dart
index 577e329..2eec984 100644
--- a/sdk/lib/ffi/ffi.dart
+++ b/sdk/lib/ffi/ffi.dart
@@ -69,7 +69,7 @@
   /// On 32-bit systems, the upper 32-bits of the result are 0.
   external int get address;
 
-  /// Cast Pointer<T> to a Pointer<V>.
+  /// Cast Pointer<T> to a Pointer<U>.
   external Pointer<U> cast<U extends NativeType>();
 
   /// Equality for Pointers only depends on their address.
@@ -1556,6 +1556,10 @@
 /// NOTE: This is an experimental feature and may change in the future.
 @Since('2.19')
 final class Native<T> {
+  // Implementation note: VM hardcodes the layout of this class (number and
+  // order of its fields), so adding/removing/changing fields requires
+  // updating the VM code (see Function::GetNativeAnnotation()).
+
   /// The native symbol to be resolved, if not using the default.
   ///
   /// If not specified, the default symbol used for native function lookup
diff --git a/tests/ffi/abi_specific_int_incomplete_jit_test.dart b/tests/ffi/abi_specific_int_incomplete_jit_test.dart
index 54f6843..9038f6e 100644
--- a/tests/ffi/abi_specific_int_incomplete_jit_test.dart
+++ b/tests/ffi/abi_specific_int_incomplete_jit_test.dart
@@ -119,21 +119,27 @@
   Expect.throws(() {
     nullptr
         .cast<NativeFunction<Int32 Function(Incomplete)>>()
-        .asFunction<int Function(int)>();
+        .asFunction<int Function(int)>()
+        .call(42);
   });
   Expect.throws(() {
     nullptr
         .cast<NativeFunction<Incomplete Function(Int32)>>()
-        .asFunction<int Function(int)>();
+        .asFunction<int Function(int)>()
+        .call(42);
   });
+  final p = calloc<Int64>(100).cast<IncompleteArrayStruct>();
   Expect.throws(() {
     nullptr
         .cast<NativeFunction<Int32 Function(IncompleteArrayStruct)>>()
-        .asFunction<int Function(IncompleteArrayStruct)>();
+        .asFunction<int Function(IncompleteArrayStruct)>()
+        .call(p.ref);
   });
+  calloc.free(p);
   Expect.throws(() {
     nullptr
         .cast<NativeFunction<IncompleteArrayStruct Function()>>()
-        .asFunction<IncompleteArrayStruct Function()>();
+        .asFunction<IncompleteArrayStruct Function()>()
+        .call();
   });
 }
diff --git a/tests/ffi/function_test.dart b/tests/ffi/function_test.dart
index 6d4c77e..1e7d5aa 100644
--- a/tests/ffi/function_test.dart
+++ b/tests/ffi/function_test.dart
@@ -459,6 +459,12 @@
   Expect.approxEquals(1337.0, result);
 }
 
+// Returns a possibly ofuscated 'arg2' identifier.
+String get arg2ObfuscatedName {
+  final str = (arg2: 0).toString();
+  return str.substring('('.length, str.length - ': 0)'.length);
+}
+
 void testNativeFunctionNullableInt() {
   final sumPlus42 = ffiTestFunctions.lookupFunction<
       Int32 Function(Int32, Int32), int Function(int, int?)>("SumPlus42");
@@ -467,7 +473,7 @@
     sumPlus42(3, null);
   } catch (e) {
     // TODO(http://dartbug.com/47098): Save param names to dwarf.
-    Expect.isTrue(e.toString().contains('ffi_param2') ||
+    Expect.isTrue(e.toString().contains(arg2ObfuscatedName) ||
         e.toString().contains('<optimized out>'));
   }
 }