[test/ffi] Test generator pointer arguments

This CL extends the test generator to support struct pointers. For
callbacks, only synchronous use of pointers is supported in tests.
(Ownership of memory is _not_ passed to Dart.) We have plenty of other
tests passing ownership. These tests are meant to check the ABIs, for
which sync calls and callbacks suffice.

Split off https://dart-review.googlesource.com/c/sdk/+/371960 to make
the changes in that CL only relate to variable length arrays. That CL
will pass pointers to structs with variable length arrays.

TEST=tests/ffi/*

Change-Id: Ib8ff7b4e1d0f2451892ea693555803682bbf3bc0
tools/find_builders.dart ffi/function_structs_by_value_generated_args_test
Cq-Include-Trybots: dart/try:vm-aot-android-release-arm64c-try,vm-aot-android-release-arm_x64-try,vm-aot-asan-linux-release-x64-try,vm-aot-linux-debug-x64-try,vm-aot-linux-debug-x64c-try,vm-aot-mac-release-arm64-try,vm-aot-mac-release-x64-try,vm-aot-msan-linux-release-x64-try,vm-aot-obfuscate-linux-release-x64-try,vm-aot-optimization-level-linux-release-x64-try,vm-aot-tsan-linux-release-x64-try,vm-aot-ubsan-linux-release-x64-try,vm-aot-win-debug-arm64-try,vm-aot-win-debug-x64-try,vm-aot-win-debug-x64c-try,vm-appjit-linux-debug-x64-try,vm-asan-linux-release-arm64-try,vm-asan-linux-release-x64-try,vm-checked-mac-release-arm64-try,vm-ffi-android-debug-arm-try,vm-ffi-android-debug-arm64c-try,vm-ffi-qemu-linux-release-arm-try,vm-ffi-qemu-linux-release-riscv64-try,vm-fuchsia-release-arm64-try,vm-fuchsia-release-x64-try,vm-linux-debug-ia32-try,vm-linux-debug-x64-try,vm-linux-debug-x64c-try,vm-mac-debug-arm64-try,vm-mac-debug-x64-try,vm-msan-linux-release-arm64-try,vm-msan-linux-release-x64-try,vm-reload-linux-debug-x64-try,vm-reload-rollback-linux-debug-x64-try,vm-tsan-linux-release-arm64-try,vm-tsan-linux-release-x64-try,vm-ubsan-linux-release-arm64-try,vm-ubsan-linux-release-x64-try,vm-win-debug-arm64-try,vm-win-debug-x64-try,vm-win-debug-x64c-try,vm-win-release-ia32-try
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/378704
Reviewed-by: Tess Strickland <sstrickl@google.com>
Reviewed-by: Liam Appelbe <liama@google.com>
Commit-Queue: Daco Harkes <dacoharkes@google.com>
diff --git a/runtime/bin/ffi_test/ffi_test_functions_generated.cc b/runtime/bin/ffi_test/ffi_test_functions_generated.cc
index 9be77c1..9b0d891 100644
--- a/runtime/bin/ffi_test/ffi_test_functions_generated.cc
+++ b/runtime/bin/ffi_test/ffi_test_functions_generated.cc
@@ -4876,6 +4876,25 @@
 }
 
 // Used for testing structs and unions by value.
+// Passing a pointer to a struct
+DART_EXPORT int64_t
+PassPointerStruct12BytesHomogeneousInt32(Struct12BytesHomogeneousInt32* a0) {
+  std::cout << "PassPointerStruct12BytesHomogeneousInt32"
+            << "((" << a0->a0 << ", " << a0->a1 << ", " << a0->a2 << "))"
+            << "\n";
+
+  int64_t result = 0;
+
+  result += a0->a0;
+  result += a0->a1;
+  result += a0->a2;
+
+  std::cout << "result = " << result << "\n";
+
+  return result;
+}
+
+// Used for testing structs and unions by value.
 // Smallest struct with data.
 DART_EXPORT Struct1ByteInt ReturnStruct1ByteInt(int8_t a0) {
   std::cout << "ReturnStruct1ByteInt"
@@ -12607,6 +12626,45 @@
 }
 
 // Used for testing structs and unions by value.
+// Passing a pointer to a struct
+DART_EXPORT intptr_t TestPassPointerStruct12BytesHomogeneousInt32(
+    // NOLINTNEXTLINE(whitespace/parens)
+    int64_t (*f)(Struct12BytesHomogeneousInt32* a0)) {
+  Struct12BytesHomogeneousInt32 a0_value = {};
+  Struct12BytesHomogeneousInt32* a0 = &a0_value;
+
+  a0->a0 = -1;
+  a0->a1 = 2;
+  a0->a2 = -3;
+
+  std::cout << "Calling TestPassPointerStruct12BytesHomogeneousInt32("
+            << "((" << a0->a0 << ", " << a0->a1 << ", " << a0->a2 << "))"
+            << ")\n";
+
+  int64_t result = f(a0);
+
+  std::cout << "result = " << result << "\n";
+
+  CHECK_EQ(-2, result);
+
+  // Pass argument that will make the Dart callback throw.
+  a0->a0 = 42;
+
+  result = f(a0);
+
+  CHECK_EQ(0, result);
+
+  // Pass argument that will make the Dart callback return null.
+  a0->a0 = 84;
+
+  result = f(a0);
+
+  CHECK_EQ(0, result);
+
+  return 0;
+}
+
+// Used for testing structs and unions by value.
 // Smallest struct with data.
 DART_EXPORT intptr_t TestReturnStruct1ByteInt(
     // NOLINTNEXTLINE(whitespace/parens)
@@ -21225,6 +21283,25 @@
 }
 
 // Used for testing structs and unions by value.
+// Passing a pointer to a struct
+DART_EXPORT void TestAsyncPassPointerStruct12BytesHomogeneousInt32(
+    // NOLINTNEXTLINE(whitespace/parens)
+    void (*f)(Struct12BytesHomogeneousInt32* a0)) {
+  Struct12BytesHomogeneousInt32 a0_value = {};
+  Struct12BytesHomogeneousInt32* a0 = &a0_value;
+
+  a0->a0 = -1;
+  a0->a1 = 2;
+  a0->a2 = -3;
+
+  std::cout << "Calling TestAsyncPassPointerStruct12BytesHomogeneousInt32("
+            << "((" << a0->a0 << ", " << a0->a1 << ", " << a0->a2 << "))"
+            << ")\n";
+
+  f(a0);
+}
+
+// Used for testing structs and unions by value.
 // Smallest struct with data.
 DART_EXPORT void TestAsyncReturnStruct1ByteInt(
     // NOLINTNEXTLINE(whitespace/parens)
diff --git a/tests/ffi/async_callback_tests_utils.dart b/tests/ffi/async_callback_tests_utils.dart
index 65f63f2..e943c29 100644
--- a/tests/ffi/async_callback_tests_utils.dart
+++ b/tests/ffi/async_callback_tests_utils.dart
@@ -35,3 +35,5 @@
 }
 
 void noChecks() {}
+
+Future<void> noChecksAsync() async {}
diff --git a/tests/ffi/function_callbacks_structs_by_value_generated_test.dart b/tests/ffi/function_callbacks_structs_by_value_generated_test.dart
index 3e785e1..147910e 100644
--- a/tests/ffi/function_callbacks_structs_by_value_generated_test.dart
+++ b/tests/ffi/function_callbacks_structs_by_value_generated_test.dart
@@ -378,6 +378,11 @@
           passInt64x7Struct12BytesHomogeneousInt32, 0),
       passInt64x7Struct12BytesHomogeneousInt32AfterCallback),
   CallbackTest.withCheck(
+      "PassPointerStruct12BytesHomogeneousInt32",
+      Pointer.fromFunction<PassPointerStruct12BytesHomogeneousInt32Type>(
+          passPointerStruct12BytesHomogeneousInt32, 0),
+      noChecks),
+  CallbackTest.withCheck(
       "ReturnStruct1ByteInt",
       Pointer.fromFunction<ReturnStruct1ByteIntType>(returnStruct1ByteInt),
       returnStruct1ByteIntAfterCallback),
@@ -7922,6 +7927,49 @@
   Expect.equals(5, result);
 }
 
+typedef PassPointerStruct12BytesHomogeneousInt32Type = Int64 Function(
+    Pointer<Struct12BytesHomogeneousInt32>);
+
+// Global variables to be able to test inputs after callback returned.
+Pointer<Struct12BytesHomogeneousInt32>
+    passPointerStruct12BytesHomogeneousInt32_a0 = nullptr;
+
+// Result variable also global, so we can delete it after the callback.
+int passPointerStruct12BytesHomogeneousInt32Result = 0;
+
+int passPointerStruct12BytesHomogeneousInt32CalculateResult() {
+  int result = 0;
+
+  result += passPointerStruct12BytesHomogeneousInt32_a0.ref.a0;
+  result += passPointerStruct12BytesHomogeneousInt32_a0.ref.a1;
+  result += passPointerStruct12BytesHomogeneousInt32_a0.ref.a2;
+
+  passPointerStruct12BytesHomogeneousInt32Result = result;
+
+  return result;
+}
+
+/// Passing a pointer to a struct
+int passPointerStruct12BytesHomogeneousInt32(
+    Pointer<Struct12BytesHomogeneousInt32> a0) {
+  print("passPointerStruct12BytesHomogeneousInt32(${a0})");
+
+  // Possibly throw.
+  if (a0.ref.a0 == 42 || a0.ref.a0 == 84) {
+    print("throwing!");
+    throw Exception(
+        "PassPointerStruct12BytesHomogeneousInt32 throwing on purpose!");
+  }
+
+  passPointerStruct12BytesHomogeneousInt32_a0 = a0;
+
+  final result = passPointerStruct12BytesHomogeneousInt32CalculateResult();
+
+  print("result = $result");
+
+  return result;
+}
+
 typedef ReturnStruct1ByteIntType = Struct1ByteInt Function(Int8);
 
 // Global variables to be able to test inputs after callback returned.
diff --git a/tests/ffi/function_callbacks_structs_by_value_native_callable_generated_test.dart b/tests/ffi/function_callbacks_structs_by_value_native_callable_generated_test.dart
index 7ad47ec..47aa721 100644
--- a/tests/ffi/function_callbacks_structs_by_value_native_callable_generated_test.dart
+++ b/tests/ffi/function_callbacks_structs_by_value_native_callable_generated_test.dart
@@ -452,6 +452,12 @@
           exceptionalReturn: 0),
       passInt64x7Struct12BytesHomogeneousInt32AfterCallback),
   CallbackTest.withCheck(
+      "PassPointerStruct12BytesHomogeneousInt32",
+      NativeCallable<PassPointerStruct12BytesHomogeneousInt32Type>.isolateLocal(
+          passPointerStruct12BytesHomogeneousInt32,
+          exceptionalReturn: 0),
+      noChecks),
+  CallbackTest.withCheck(
       "ReturnStruct1ByteInt",
       NativeCallable<ReturnStruct1ByteIntType>.isolateLocal(
           returnStruct1ByteInt),
@@ -8006,6 +8012,49 @@
   Expect.equals(5, result);
 }
 
+typedef PassPointerStruct12BytesHomogeneousInt32Type = Int64 Function(
+    Pointer<Struct12BytesHomogeneousInt32>);
+
+// Global variables to be able to test inputs after callback returned.
+Pointer<Struct12BytesHomogeneousInt32>
+    passPointerStruct12BytesHomogeneousInt32_a0 = nullptr;
+
+// Result variable also global, so we can delete it after the callback.
+int passPointerStruct12BytesHomogeneousInt32Result = 0;
+
+int passPointerStruct12BytesHomogeneousInt32CalculateResult() {
+  int result = 0;
+
+  result += passPointerStruct12BytesHomogeneousInt32_a0.ref.a0;
+  result += passPointerStruct12BytesHomogeneousInt32_a0.ref.a1;
+  result += passPointerStruct12BytesHomogeneousInt32_a0.ref.a2;
+
+  passPointerStruct12BytesHomogeneousInt32Result = result;
+
+  return result;
+}
+
+/// Passing a pointer to a struct
+int passPointerStruct12BytesHomogeneousInt32(
+    Pointer<Struct12BytesHomogeneousInt32> a0) {
+  print("passPointerStruct12BytesHomogeneousInt32(${a0})");
+
+  // Possibly throw.
+  if (a0.ref.a0 == 42 || a0.ref.a0 == 84) {
+    print("throwing!");
+    throw Exception(
+        "PassPointerStruct12BytesHomogeneousInt32 throwing on purpose!");
+  }
+
+  passPointerStruct12BytesHomogeneousInt32_a0 = a0;
+
+  final result = passPointerStruct12BytesHomogeneousInt32CalculateResult();
+
+  print("result = $result");
+
+  return result;
+}
+
 typedef ReturnStruct1ByteIntType = Struct1ByteInt Function(Int8);
 
 // Global variables to be able to test inputs after callback returned.
diff --git a/tests/ffi/function_structs_by_value_generated_args_leaf_test.dart b/tests/ffi/function_structs_by_value_generated_args_leaf_test.dart
index 3c5ea10..9b7ba35 100644
--- a/tests/ffi/function_structs_by_value_generated_args_leaf_test.dart
+++ b/tests/ffi/function_structs_by_value_generated_args_leaf_test.dart
@@ -92,6 +92,7 @@
     testPassUint8Struct1ByteBoolLeaf();
     testPassWCharStructInlineArrayIntUintPtrx2LongUnsignedLeaf();
     testPassInt64x7Struct12BytesHomogeneousInt32Leaf();
+    testPassPointerStruct12BytesHomogeneousInt32Leaf();
   }
 }
 
@@ -5498,3 +5499,27 @@
 
   calloc.free(a7Pointer);
 }
+
+final passPointerStruct12BytesHomogeneousInt32Leaf =
+    ffiTestFunctions.lookupFunction<
+            Int64 Function(Pointer<Struct12BytesHomogeneousInt32>),
+            int Function(Pointer<Struct12BytesHomogeneousInt32>)>(
+        "PassPointerStruct12BytesHomogeneousInt32",
+        isLeaf: true);
+
+/// Passing a pointer to a struct
+void testPassPointerStruct12BytesHomogeneousInt32Leaf() {
+  final a0 = calloc<Struct12BytesHomogeneousInt32>();
+
+  a0.ref.a0 = -1;
+  a0.ref.a1 = 2;
+  a0.ref.a2 = -3;
+
+  final result = passPointerStruct12BytesHomogeneousInt32Leaf(a0);
+
+  print("result = $result");
+
+  Expect.equals(-2, result);
+
+  calloc.free(a0);
+}
diff --git a/tests/ffi/function_structs_by_value_generated_args_native_leaf_test.dart b/tests/ffi/function_structs_by_value_generated_args_native_leaf_test.dart
index 91089ce..012b6a5 100644
--- a/tests/ffi/function_structs_by_value_generated_args_native_leaf_test.dart
+++ b/tests/ffi/function_structs_by_value_generated_args_native_leaf_test.dart
@@ -95,6 +95,7 @@
     testPassUint8Struct1ByteBoolNativeLeaf();
     testPassWCharStructInlineArrayIntUintPtrx2LongUnsignedNativeLeaf();
     testPassInt64x7Struct12BytesHomogeneousInt32NativeLeaf();
+    testPassPointerStruct12BytesHomogeneousInt32NativeLeaf();
   }
 }
 
@@ -5462,3 +5463,25 @@
 
   calloc.free(a7Pointer);
 }
+
+@Native<Int64 Function(Pointer<Struct12BytesHomogeneousInt32>)>(
+    symbol: 'PassPointerStruct12BytesHomogeneousInt32', isLeaf: true)
+external int passPointerStruct12BytesHomogeneousInt32NativeLeaf(
+    Pointer<Struct12BytesHomogeneousInt32> a0);
+
+/// Passing a pointer to a struct
+void testPassPointerStruct12BytesHomogeneousInt32NativeLeaf() {
+  final a0 = calloc<Struct12BytesHomogeneousInt32>();
+
+  a0.ref.a0 = -1;
+  a0.ref.a1 = 2;
+  a0.ref.a2 = -3;
+
+  final result = passPointerStruct12BytesHomogeneousInt32NativeLeaf(a0);
+
+  print("result = $result");
+
+  Expect.equals(-2, result);
+
+  calloc.free(a0);
+}
diff --git a/tests/ffi/function_structs_by_value_generated_args_native_test.dart b/tests/ffi/function_structs_by_value_generated_args_native_test.dart
index c846e5c..368e0d7 100644
--- a/tests/ffi/function_structs_by_value_generated_args_native_test.dart
+++ b/tests/ffi/function_structs_by_value_generated_args_native_test.dart
@@ -95,6 +95,7 @@
     testPassUint8Struct1ByteBoolNative();
     testPassWCharStructInlineArrayIntUintPtrx2LongUnsignedNative();
     testPassInt64x7Struct12BytesHomogeneousInt32Native();
+    testPassPointerStruct12BytesHomogeneousInt32Native();
   }
 }
 
@@ -5472,3 +5473,25 @@
 
   calloc.free(a7Pointer);
 }
+
+@Native<Int64 Function(Pointer<Struct12BytesHomogeneousInt32>)>(
+    symbol: 'PassPointerStruct12BytesHomogeneousInt32')
+external int passPointerStruct12BytesHomogeneousInt32Native(
+    Pointer<Struct12BytesHomogeneousInt32> a0);
+
+/// Passing a pointer to a struct
+void testPassPointerStruct12BytesHomogeneousInt32Native() {
+  final a0 = calloc<Struct12BytesHomogeneousInt32>();
+
+  a0.ref.a0 = -1;
+  a0.ref.a1 = 2;
+  a0.ref.a2 = -3;
+
+  final result = passPointerStruct12BytesHomogeneousInt32Native(a0);
+
+  print("result = $result");
+
+  Expect.equals(-2, result);
+
+  calloc.free(a0);
+}
diff --git a/tests/ffi/function_structs_by_value_generated_args_test.dart b/tests/ffi/function_structs_by_value_generated_args_test.dart
index ba7ae69..6536956 100644
--- a/tests/ffi/function_structs_by_value_generated_args_test.dart
+++ b/tests/ffi/function_structs_by_value_generated_args_test.dart
@@ -92,6 +92,7 @@
     testPassUint8Struct1ByteBool();
     testPassWCharStructInlineArrayIntUintPtrx2LongUnsigned();
     testPassInt64x7Struct12BytesHomogeneousInt32();
+    testPassPointerStruct12BytesHomogeneousInt32();
   }
 }
 
@@ -5420,3 +5421,26 @@
 
   calloc.free(a7Pointer);
 }
+
+final passPointerStruct12BytesHomogeneousInt32 =
+    ffiTestFunctions.lookupFunction<
+            Int64 Function(Pointer<Struct12BytesHomogeneousInt32>),
+            int Function(Pointer<Struct12BytesHomogeneousInt32>)>(
+        "PassPointerStruct12BytesHomogeneousInt32");
+
+/// Passing a pointer to a struct
+void testPassPointerStruct12BytesHomogeneousInt32() {
+  final a0 = calloc<Struct12BytesHomogeneousInt32>();
+
+  a0.ref.a0 = -1;
+  a0.ref.a1 = 2;
+  a0.ref.a2 = -3;
+
+  final result = passPointerStruct12BytesHomogeneousInt32(a0);
+
+  print("result = $result");
+
+  Expect.equals(-2, result);
+
+  calloc.free(a0);
+}
diff --git a/tests/ffi/generator/c_types.dart b/tests/ffi/generator/c_types.dart
index 4afd2a2..9bc255e 100644
--- a/tests/ffi/generator/c_types.dart
+++ b/tests/ffi/generator/c_types.dart
@@ -588,7 +588,9 @@
     }
 
     for (final group in argumentsGrouped) {
-      result += group.first.type.dartCType;
+      final dartCType =
+          group.first.type.dartCType.replaceAll('<', '').replaceAll('>', '');
+      result += dartCType;
       if (group.length > 1) {
         result += "x${group.length}";
       }
@@ -606,8 +608,22 @@
 }
 
 extension MemberList on List<Member> {
-  bool get containsComposites =>
-      map((m) => m.type is CompositeType).contains(true);
+  bool get containsComposites => map((m) {
+        final type = m.type;
+        switch (type) {
+          case CompositeType _:
+            return true;
+          case PointerType _:
+            final pointerTo = type.pointerTo;
+            switch (pointerTo) {
+              case CompositeType _:
+                return true;
+            }
+        }
+        return false;
+      }).contains(true);
+
+  bool get containsPointers => any((m) => m.type is PointerType);
 }
 
 extension ListT<T> on List<T> {
diff --git a/tests/ffi/generator/structs_by_value_tests_configuration.dart b/tests/ffi/generator/structs_by_value_tests_configuration.dart
index d1a0043..f217b7e 100644
--- a/tests/ffi/generator/structs_by_value_tests_configuration.dart
+++ b/tests/ffi/generator/structs_by_value_tests_configuration.dart
@@ -450,6 +450,13 @@
       int64,
       """
 Struct stradles last argument register"""),
+  FunctionType(
+      [
+        PointerType(struct12bytesInt),
+      ],
+      int64,
+      """
+Passing a pointer to a struct"""),
 ];
 
 /// Functions that return a struct by value.
diff --git a/tests/ffi/generator/structs_by_value_tests_generator.dart b/tests/ffi/generator/structs_by_value_tests_generator.dart
index 1b72f51..077a9e3 100644
--- a/tests/ffi/generator/structs_by_value_tests_generator.dart
+++ b/tests/ffi/generator/structs_by_value_tests_generator.dart
@@ -63,6 +63,14 @@
         final this_ = this as StructType;
         return this_.members.coutExpression("$variableName.");
 
+      case PointerType:
+        final this_ = this as PointerType;
+        final pointerTo = this_.pointerTo;
+        switch (pointerTo) {
+          case StructType _:
+            return pointerTo.members.coutExpression("$variableName->");
+        }
+
       case UnionType:
         final this_ = this as UnionType;
         return this_.members.take(1).toList().coutExpression("$variableName.");
@@ -105,7 +113,7 @@
   /// A list of statements adding all members recursively to `result`.
   ///
   /// Both valid in Dart and C.
-  String addToResultStatements(String variableName) {
+  String addToResultStatements(String variableName, bool isDart) {
     switch (this.runtimeType) {
       case FundamentalType:
         final this_ = this as FundamentalType;
@@ -114,20 +122,33 @@
 
       case StructType:
         final this_ = this as StructType;
-        return this_.members.addToResultStatements("$variableName.");
+        return this_.members.addToResultStatements(isDart, "$variableName.");
+
+      case PointerType:
+        final this_ = this as PointerType;
+        final pointerTo = this_.pointerTo;
+        switch (pointerTo) {
+          case StructType _:
+            if (isDart) {
+              return pointerTo.members
+                  .addToResultStatements(isDart, "$variableName.ref.");
+            }
+            return pointerTo.members
+                .addToResultStatements(isDart, "$variableName->");
+        }
 
       case UnionType:
         final this_ = this as UnionType;
         final member = this_.members.first;
         return member.type
-            .addToResultStatements("$variableName.${member.name}");
+            .addToResultStatements("$variableName.${member.name}", isDart);
 
       case FixedLengthArrayType:
         final this_ = this as FixedLengthArrayType;
         final indices = [for (var i = 0; i < this_.length; i += 1) i];
         return indices
-            .map((i) =>
-                this_.elementType.addToResultStatements("$variableName[$i]"))
+            .map((i) => this_.elementType
+                .addToResultStatements("$variableName[$i]", isDart))
             .join();
     }
 
@@ -139,8 +160,9 @@
   /// A list of statements adding all members recursively to `result`.
   ///
   /// Both valid in Dart and C.
-  String addToResultStatements([String namePrefix = ""]) {
-    return map((m) => m.type.addToResultStatements("$namePrefix${m.name}"))
+  String addToResultStatements(bool isDart, [String namePrefix = ""]) {
+    return map(
+            (m) => m.type.addToResultStatements("$namePrefix${m.name}", isDart))
         .join();
   }
 }
@@ -149,7 +171,11 @@
   /// A list of statements recursively assigning all members with [a].
   ///
   /// Both valid in Dart and C.
-  String assignValueStatements(ArgumentValueAssigner a, String variableName) {
+  String assignValueStatements(
+    ArgumentValueAssigner a,
+    String variableName,
+    bool isDart,
+  ) {
     switch (this.runtimeType) {
       case FundamentalType:
         final this_ = this as FundamentalType;
@@ -157,21 +183,33 @@
 
       case StructType:
         final this_ = this as StructType;
-        return this_.members.assignValueStatements(a, "$variableName.");
+        return this_.members.assignValueStatements(a, isDart, "$variableName.");
 
       case UnionType:
         final this_ = this as UnionType;
         final member = this_.members.first;
         return member.type
-            .assignValueStatements(a, "$variableName.${member.name}");
+            .assignValueStatements(a, "$variableName.${member.name}", isDart);
 
       case FixedLengthArrayType:
         final this_ = this as FixedLengthArrayType;
         final indices = [for (var i = 0; i < this_.length; i += 1) i];
         return indices
-            .map((i) =>
-                this_.elementType.assignValueStatements(a, "$variableName[$i]"))
+            .map((i) => this_.elementType
+                .assignValueStatements(a, "$variableName[$i]", isDart))
             .join();
+      case PointerType:
+        final this_ = this as PointerType;
+        final pointerTo = this_.pointerTo;
+        switch (pointerTo) {
+          case StructType _:
+            if (isDart) {
+              return pointerTo.members
+                  .assignValueStatements(a, isDart, "$variableName.ref.");
+            }
+            return pointerTo.members
+                .assignValueStatements(a, isDart, "$variableName->");
+        }
     }
 
     throw Exception("Not implemented for ${this.runtimeType}");
@@ -198,10 +236,16 @@
   /// A list of statements recursively assigning all members with [a].
   ///
   /// Both valid in Dart and C.
-  String assignValueStatements(ArgumentValueAssigner a,
-      [String namePrefix = ""]) {
-    return map((m) => m.type.assignValueStatements(a, "$namePrefix${m.name}"))
-        .join();
+  String assignValueStatements(
+    ArgumentValueAssigner a,
+    bool isDart, [
+    String namePrefix = "",
+  ]) {
+    return map((m) => m.type.assignValueStatements(
+          a,
+          "$namePrefix${m.name}",
+          isDart,
+        )).join();
   }
 
   /// A list of statements recursively coping all members from [source].
@@ -252,6 +296,17 @@
   /// A list of Dart statements recursively allocating all members.
   String dartAllocateStatements(String variableName) {
     switch (this.runtimeType) {
+      case PointerType:
+        final this_ = this as PointerType;
+        final pointerTo = this_.pointerTo;
+        switch (pointerTo) {
+          case StructType _:
+            return '''
+  final ${variableName} = calloc<${pointerTo.dartType}>();
+  ''';
+        }
+        return "\n";
+
       case FundamentalType:
         return "  ${dartType} ${variableName};\n";
 
@@ -290,6 +345,9 @@
         } else {
           return "${dartType} ${variableName} = Pointer<${dartType}>.fromAddress(0).ref;\n";
         }
+
+      case PointerType:
+        return "${dartType} ${variableName} = nullptr;\n";
     }
 
     throw Exception("Not implemented for ${this.runtimeType}");
@@ -320,6 +378,9 @@
       case StructType:
       case UnionType:
         return "calloc.free(${variableName}Pointer);\n";
+
+      case PointerType:
+        return "calloc.free(${variableName});\n";
     }
 
     throw Exception("Not implemented for ${this.runtimeType}");
@@ -342,6 +403,16 @@
       case StructType:
       case UnionType:
         return "${cType} ${variableName} = {};\n";
+      case PointerType:
+        final this_ = this as PointerType;
+        final pointerTo = this_.pointerTo;
+        switch (pointerTo) {
+          case StructType _:
+            return '''
+${pointerTo.cType} ${variableName}_value = {};
+${cType} ${variableName} = &${variableName}_value;
+''';
+        }
     }
 
     throw Exception("Not implemented for ${this.runtimeType}");
@@ -474,7 +545,7 @@
   /// Expression denoting the first FundamentalType field.
   ///
   /// Both valid in Dart and C.
-  String firstArgumentName(String variableName) {
+  String firstArgumentName(String variableName, bool isDart) {
     switch (this.runtimeType) {
       case FundamentalType:
         return variableName;
@@ -482,11 +553,24 @@
       case StructType:
       case UnionType:
         final this_ = this as CompositeType;
-        return this_.members.firstArgumentName("$variableName.");
+        return this_.members.firstArgumentName(isDart, "$variableName.");
+
+      case PointerType:
+        final this_ = this as PointerType;
+        final pointerTo = this_.pointerTo;
+        switch (pointerTo) {
+          case StructType _:
+            if (isDart) {
+              return pointerTo.members
+                  .firstArgumentName(isDart, "$variableName.ref.");
+            }
+            return pointerTo.members
+                .firstArgumentName(isDart, "$variableName->");
+        }
 
       case FixedLengthArrayType:
         final this_ = this as FixedLengthArrayType;
-        return this_.elementType.firstArgumentName("$variableName[0]");
+        return this_.elementType.firstArgumentName("$variableName[0]", isDart);
     }
 
     throw Exception("Not implemented for ${this.runtimeType}");
@@ -497,8 +581,8 @@
   /// Expression denoting the first FundamentalType field.
   ///
   /// Both valid in Dart and C.
-  String firstArgumentName([String prefix = ""]) {
-    return this[0].type.firstArgumentName("$prefix${this[0].name}");
+  String firstArgumentName(bool isDart, [String prefix = ""]) {
+    return this[0].type.firstArgumentName("$prefix${this[0].name}", isDart);
   }
 }
 
@@ -565,7 +649,7 @@
 extension on FunctionType {
   String dartCallCode({required bool isLeaf, required bool isNative}) {
     final a = ArgumentValueAssigner();
-    final assignValues = arguments.assignValueStatements(a);
+    final assignValues = arguments.assignValueStatements(a, true);
     final argumentFrees = arguments.dartFreeStatements();
 
     final argumentNames = arguments.map((e) => e.name).join(", ");
@@ -639,7 +723,7 @@
         buildReturnValue = """
   $returnValueType result = 0;
 
-${arguments.addToResultStatements('${dartName}_')}
+${arguments.addToResultStatements(true, '${dartName}_')}
   """;
         assignReturnGlobal = "${dartName}Result = $result;";
         break;
@@ -664,13 +748,21 @@
 
     final globals = arguments.dartAllocateZeroStatements("${dartName}_");
 
-    final copyToGlobals =
-        arguments.map((a) => '${dartName}_${a.name} = ${a.name};').join("\n  ");
+    final copyToGlobals = arguments.map((a) {
+      final type = a.type;
+      switch (type) {
+        case PointerType _:
+          // Copy the pointer, but don't use after callback returned.
+          return '${dartName}_${a.name} = ${a.name};';
+        default:
+          return '${dartName}_${a.name} = ${a.name};';
+      }
+    }).join("\n  ");
 
     // Simulate assigning values the same way as in C, so that we know what the
     // final return value should be.
     final a = ArgumentValueAssigner();
-    arguments.assignValueStatements(a);
+    arguments.assignValueStatements(a, true);
     String afterCallbackExpects = "";
     String afterCallbackFrees = "";
     switch (testType) {
@@ -711,8 +803,8 @@
   print("$dartName($prints)");
 
   // Possibly throw.
-  if (${arguments.firstArgumentName()} == $throwExceptionValue ||
-      ${arguments.firstArgumentName()} == $returnNullValue) {
+  if (${arguments.firstArgumentName(true)} == $throwExceptionValue ||
+      ${arguments.firstArgumentName(true)} == $returnNullValue) {
     print("throwing!");
     throw Exception("$cName throwing on purpose!");
   }
@@ -726,6 +818,7 @@
   return result;
 }
 
+${this.arguments.containsPointers ? '' : '''
 void ${dartName}AfterCallback() {
   $afterCallbackFrees
 
@@ -737,6 +830,7 @@
 
   $afterCallbackFrees
 }
+'''}
 
 """;
   }
@@ -760,10 +854,14 @@
     final constructor = isNativeCallable
         ? 'NativeCallable<$T>.isolateLocal'
         : 'Pointer.fromFunction<$T>';
+    final afterCallback = this.arguments.containsPointers
+        ? 'noChecks'
+        : '${dartName}AfterCallback';
+
     return """
   CallbackTest.withCheck("$cName",
     $constructor($dartName$exceptionalReturn),
-    ${dartName}AfterCallback),
+    $afterCallback),
 """;
   }
 
@@ -778,7 +876,7 @@
         FunctionType(argumentTypes, void_, reason, varArgsIndex: varArgsIndex);
 
     final a = ArgumentValueAssigner();
-    arguments.assignValueStatements(a);
+    arguments.assignValueStatements(a, true);
     final expectedResult = a.sumValue(int64);
 
     return """
@@ -793,7 +891,7 @@
 
   double result = 0;
 
-${arguments.addToResultStatements()}
+${arguments.addToResultStatements(true)}
 
   print("result = \$result");
   ${cName}Result.complete(result);
@@ -812,10 +910,13 @@
     final T = '${cName}Type';
     final constructor =
         isAsync ? 'NativeCallable<$T>.listener' : 'Pointer.fromFunction<$T>';
+    final afterCallback = this.arguments.containsPointers
+        ? 'noChecksAsync'
+        : '${dartName}AfterCallback';
     return """
   AsyncCallbackTest("$cName",
     $constructor($dartName),
-    ${dartName}AfterCallback),
+    $afterCallback),
 """;
   }
 
@@ -833,7 +934,7 @@
         body = """
         $returnValueType result = 0;
 
-        ${arguments.addToResultStatements()}
+        ${arguments.addToResultStatements(false)}
         """;
         break;
       case TestType.structReturn:
@@ -892,7 +993,7 @@
   String get cCallbackCode {
     final a = ArgumentValueAssigner();
     final argumentAllocations = arguments.cAllocateStatements();
-    final assignValues = arguments.assignValueStatements(a);
+    final assignValues = arguments.assignValueStatements(a, false);
 
     final argumentString = [
       for (final argument in arguments.take(varArgsIndex ?? arguments.length))
@@ -948,14 +1049,14 @@
       $expects
 
       // Pass argument that will make the Dart callback throw.
-      ${arguments.firstArgumentName()} = $throwExceptionValue;
+      ${arguments.firstArgumentName(false)} = $throwExceptionValue;
 
       result = f($argumentNames);
 
       $expectsZero
 
       // Pass argument that will make the Dart callback return null.
-      ${arguments.firstArgumentName()} = $returnNullValue;
+      ${arguments.firstArgumentName(false)} = $returnNullValue;
 
       result = f($argumentNames);
 
@@ -970,7 +1071,7 @@
   String get cAsyncCallbackCode {
     final a = ArgumentValueAssigner();
     final argumentAllocations = arguments.cAllocateStatements();
-    final assignValues = arguments.assignValueStatements(a);
+    final assignValues = arguments.assignValueStatements(a, false);
 
     final argumentString = [
       for (final argument in arguments.take(varArgsIndex ?? arguments.length))
@@ -1245,10 +1346,15 @@
 """;
 }
 
-Future<void> writeDartNativeCallableListenerTest(List<FunctionType> functions,
-    {required bool isAsync}) async {
+Future<void> writeDartNativeCallableListenerTest(
+  List<FunctionType> functions, {
+  required bool isAsync,
+}) async {
   final StringBuffer buffer = StringBuffer();
   buffer.write(headerDartAsyncCallbackTest());
+  if (isAsync) {
+    functions = functions.where((f) => !f.arguments.containsPointers).toList();
+  }
   final constructors = functions
       .map((e) => e.dartNativeCallableListenerTestConstructor(isAsync: isAsync))
       .join("\n");
diff --git a/tests/ffi/native_callables_sync_structs_by_value_generated_test.dart b/tests/ffi/native_callables_sync_structs_by_value_generated_test.dart
index cc293ae..66a13fb 100644
--- a/tests/ffi/native_callables_sync_structs_by_value_generated_test.dart
+++ b/tests/ffi/native_callables_sync_structs_by_value_generated_test.dart
@@ -378,6 +378,11 @@
           passInt64x7Struct12BytesHomogeneousInt32),
       passInt64x7Struct12BytesHomogeneousInt32AfterCallback),
   AsyncCallbackTest(
+      "PassPointerStruct12BytesHomogeneousInt32",
+      Pointer.fromFunction<PassPointerStruct12BytesHomogeneousInt32Type>(
+          passPointerStruct12BytesHomogeneousInt32),
+      noChecksAsync),
+  AsyncCallbackTest(
       "ReturnStruct1ByteInt",
       Pointer.fromFunction<ReturnStruct1ByteIntType>(returnStruct1ByteInt),
       returnStruct1ByteIntAfterCallback),
@@ -5135,6 +5140,33 @@
   Expect.approxEquals(5, result);
 }
 
+typedef PassPointerStruct12BytesHomogeneousInt32Type = Void Function(
+    Pointer<Struct12BytesHomogeneousInt32>);
+
+// Global variable that stores the result.
+final PassPointerStruct12BytesHomogeneousInt32Result = Completer<double>();
+
+/// Passing a pointer to a struct
+void passPointerStruct12BytesHomogeneousInt32(
+    Pointer<Struct12BytesHomogeneousInt32> a0) {
+  print("passPointerStruct12BytesHomogeneousInt32(${a0})");
+
+  double result = 0;
+
+  result += a0.ref.a0;
+  result += a0.ref.a1;
+  result += a0.ref.a2;
+
+  print("result = $result");
+  PassPointerStruct12BytesHomogeneousInt32Result.complete(result);
+}
+
+Future<void> passPointerStruct12BytesHomogeneousInt32AfterCallback() async {
+  final result = await PassPointerStruct12BytesHomogeneousInt32Result.future;
+  print("after callback result = $result");
+  Expect.approxEquals(-2, result);
+}
+
 typedef ReturnStruct1ByteIntType = Void Function(Int8);
 
 // Global variable that stores the result.