[vm/ffi] Add embedder API for resolving asset ids

Extends the `NativeAssetsApi` with a `dlopen` that takes an asset id
instead of an asset path. This enables the embedder (instead of the
vm) to resolve the asset id to asset path.

Additionally, the `NativeAssetsApi` gets an `available_assets`
callback to report which asset ids are available if asset resolution
failed. (Otherwise, users would lose this important part of the error
message on failed resolution.)

We postpone migrating the Dart standalone embedder to this API: https://dart-review.googlesource.com/c/sdk/+/388160. That CL verifies
this new API and it's implementation. Without that CL this new API is
hard to test. So, no further tests are added here.

Tests that verify the old behavior for the standalone embedder:

TEST=tests/ffi/native_assets/*
TEST=pkg/dartdev/test/native_assets/*

Bug: https://github.com/flutter/flutter/issues/154425
Change-Id: I04d0fb45dc5663e63d91b21a6f5764929f10aaff
Cq-Include-Trybots: dart/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-obfuscate-linux-release-x64-try,vm-aot-optimization-level-linux-release-x64-try,vm-aot-win-debug-arm64-try,vm-aot-win-debug-x64-try,vm-aot-win-debug-x64c-try,pkg-linux-debug-try,pkg-linux-release-arm64-try,pkg-mac-release-try,pkg-mac-release-arm64-try,pkg-win-release-try,pkg-win-release-arm64-try,vm-aot-asan-linux-release-x64-try,vm-asan-linux-release-x64-try,vm-aot-msan-linux-release-x64-try,vm-msan-linux-release-x64-try
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/388161
Commit-Queue: Daco Harkes <dacoharkes@google.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
diff --git a/runtime/include/dart_api.h b/runtime/include/dart_api.h
index 035c909..1d83c90 100644
--- a/runtime/include/dart_api.h
+++ b/runtime/include/dart_api.h
@@ -3344,21 +3344,17 @@
 /**
  * Callback provided by the embedder that is used by the VM to resolve asset
  * paths.
- * If no callback is provided, using `@Native`s with `native_asset.yaml`s will
- * fail.
  *
  * The VM is responsible for looking up the asset path with the asset id in the
- * kernel mapping.
- * The embedder is responsible for providing the asset mapping during kernel
- * compilation and using the asset path to return a library handle in this
- * function.
+ * kernel mapping. The embedder is responsible for providing the asset mapping
+ * during kernel compilation and using the asset path to return a library handle
+ * in this function.
  *
  * \param path The string in the asset path as passed in native_assets.yaml
  *             during kernel compilation.
  *
- * \param error Returns NULL if creation is successful, an error message
- *   otherwise. The caller is responsible for calling free() on the error
- *   message.
+ * \param error Returns NULL if successful, an error message otherwise. The
+ *   caller is responsible for calling free() on the error message.
  *
  * \return The library handle. If |error| is not-null, the return value is
  *         undefined.
@@ -3368,6 +3364,38 @@
 typedef void* (*Dart_NativeAssetsDlopenCallbackNoPath)(char** error);
 
 /**
+ * Callback provided by the embedder that is used by the VM to resolve asset
+ * ids.
+ *
+ * The embedder can freely chose how to bundle asset id to asset path mappings
+ * and how to perform this lookup.
+ *
+ * If the embedder provides this callback, it must also provide
+ * `Dart_NativeAssetsAvailableAssets`.
+ *
+ * If provided, takes prescedence over `Dart_NativeAssetsDlopenCallback`.
+ *
+ * \param path The asset id requested in the `@Native` external function.
+ *
+ * \param error Returns NULL if successful, an error message otherwise. The
+ *   caller is responsible for calling free() on the error message.
+ *
+ * \return The library handle. If |error| is not-null, the return value is
+ *         undefined.
+ */
+typedef void* (*Dart_NativeAssetsDlopenAssetId)(const char* asset_id,
+                                                char** error);
+
+/**
+ * Callback provided by the embedder that is used  by the VM to request a
+ * description of the available assets
+ *
+ * \return A malloced string containing all asset ids. The caller must free this
+ *   string.
+ */
+typedef char* (*Dart_NativeAssetsAvailableAssets)();
+
+/**
  * Callback provided by the embedder that is used by the VM to lookup symbols
  * in native code assets.
  * If no callback is provided, using `@Native`s with `native_asset.yaml`s will
@@ -3397,6 +3425,8 @@
   Dart_NativeAssetsDlopenCallbackNoPath dlopen_process;
   Dart_NativeAssetsDlopenCallbackNoPath dlopen_executable;
   Dart_NativeAssetsDlsymCallback dlsym;
+  Dart_NativeAssetsDlopenAssetId dlopen;
+  Dart_NativeAssetsAvailableAssets available_assets;
 } NativeAssetsApi;
 
 /**
diff --git a/runtime/lib/ffi_dynamic_library.cc b/runtime/lib/ffi_dynamic_library.cc
index 8f6eabe..8e51bd1 100644
--- a/runtime/lib/ffi_dynamic_library.cc
+++ b/runtime/lib/ffi_dynamic_library.cc
@@ -317,6 +317,7 @@
     while (it.MoveNext()) {
       if (!first) {
         buffer.Printf(" ,");
+        first = false;
       }
       auto entry = it.Current();
       asset_id ^= map.GetKey(entry);
@@ -335,66 +336,81 @@
 // ['<path_type>', '<path (optional)>']
 // The |asset_location| is conform to: pkg/vm/lib/native_assets/validator.dart
 static void* FfiResolveAsset(Thread* const thread,
-                             const Array& asset_location,
+                             const String& asset,
                              const String& symbol,
                              char** error) {
-  Zone* const zone = thread->zone();
-
-  const auto& asset_type =
-      String::Cast(Object::Handle(zone, asset_location.At(0)));
-  String& path = String::Handle(zone);
-  const char* path_cstr = nullptr;
-  if (asset_type.Equals(Symbols::absolute()) ||
-      asset_type.Equals(Symbols::relative()) ||
-      asset_type.Equals(Symbols::system())) {
-    path = String::RawCast(asset_location.At(1));
-    path_cstr = path.ToCString();
-  }
-
+  void* handle = nullptr;
   NativeAssetsApi* native_assets_api =
       thread->isolate_group()->native_assets_api();
-  void* handle;
-  if (asset_type.Equals(Symbols::absolute())) {
-    if (native_assets_api->dlopen_absolute == nullptr) {
-      *error = OS::SCreate(/*use malloc*/ nullptr,
-                           "NativeAssetsApi::dlopen_absolute not set.");
+  if (native_assets_api->dlopen != nullptr) {
+    // Let embedder resolve the asset id to asset path.
+    NoActiveIsolateScope no_active_isolate_scope;
+    handle = native_assets_api->dlopen(asset.ToCString(), error);
+  }
+  if (*error == nullptr && handle == nullptr) {
+    // Fall back on VM reading ffi:native-assets from special library in kernel.
+    // Allow for both embedder and VM resolution so flutter/engine and
+    // flutter/flutter PRs can land without manual roll.
+    Zone* const zone = thread->zone();
+    const auto& asset_location =
+        Array::Handle(zone, GetAssetLocation(thread, asset));
+    if (asset_location.IsNull()) {
       return nullptr;
     }
-    NoActiveIsolateScope no_active_isolate_scope;
-    handle = native_assets_api->dlopen_absolute(path_cstr, error);
-  } else if (asset_type.Equals(Symbols::relative())) {
-    if (native_assets_api->dlopen_relative == nullptr) {
-      *error = OS::SCreate(/*use malloc*/ nullptr,
-                           "NativeAssetsApi::dlopen_relative not set.");
-      return nullptr;
+
+    const auto& asset_type =
+        String::Cast(Object::Handle(zone, asset_location.At(0)));
+    String& path = String::Handle(zone);
+    const char* path_cstr = nullptr;
+    if (asset_type.Equals(Symbols::absolute()) ||
+        asset_type.Equals(Symbols::relative()) ||
+        asset_type.Equals(Symbols::system())) {
+      path = String::RawCast(asset_location.At(1));
+      path_cstr = path.ToCString();
     }
-    NoActiveIsolateScope no_active_isolate_scope;
-    handle = native_assets_api->dlopen_relative(path_cstr, error);
-  } else if (asset_type.Equals(Symbols::system())) {
-    if (native_assets_api->dlopen_system == nullptr) {
-      *error = OS::SCreate(/*use malloc*/ nullptr,
-                           "NativeAssetsApi::dlopen_system not set.");
-      return nullptr;
+
+    if (asset_type.Equals(Symbols::absolute())) {
+      if (native_assets_api->dlopen_absolute == nullptr) {
+        *error = OS::SCreate(/*use malloc*/ nullptr,
+                             "NativeAssetsApi::dlopen_absolute not set.");
+        return nullptr;
+      }
+      NoActiveIsolateScope no_active_isolate_scope;
+      handle = native_assets_api->dlopen_absolute(path_cstr, error);
+    } else if (asset_type.Equals(Symbols::relative())) {
+      if (native_assets_api->dlopen_relative == nullptr) {
+        *error = OS::SCreate(/*use malloc*/ nullptr,
+                             "NativeAssetsApi::dlopen_relative not set.");
+        return nullptr;
+      }
+      NoActiveIsolateScope no_active_isolate_scope;
+      handle = native_assets_api->dlopen_relative(path_cstr, error);
+    } else if (asset_type.Equals(Symbols::system())) {
+      if (native_assets_api->dlopen_system == nullptr) {
+        *error = OS::SCreate(/*use malloc*/ nullptr,
+                             "NativeAssetsApi::dlopen_system not set.");
+        return nullptr;
+      }
+      NoActiveIsolateScope no_active_isolate_scope;
+      handle = native_assets_api->dlopen_system(path_cstr, error);
+    } else if (asset_type.Equals(Symbols::executable())) {
+      if (native_assets_api->dlopen_executable == nullptr) {
+        *error = OS::SCreate(/*use malloc*/ nullptr,
+                             "NativeAssetsApi::dlopen_executable not set.");
+        return nullptr;
+      }
+      NoActiveIsolateScope no_active_isolate_scope;
+      handle = native_assets_api->dlopen_executable(error);
+    } else {
+      RELEASE_ASSERT(asset_type.Equals(Symbols::process()));
+      if (native_assets_api->dlopen_process == nullptr) {
+        *error = OS::SCreate(/*use malloc*/ nullptr,
+                             "NativeAssetsApi::dlopen_process not set.");
+        return nullptr;
+      }
+      NoActiveIsolateScope no_active_isolate_scope;
+      handle = native_assets_api->dlopen_process(error);
     }
-    NoActiveIsolateScope no_active_isolate_scope;
-    handle = native_assets_api->dlopen_system(path_cstr, error);
-  } else if (asset_type.Equals(Symbols::executable())) {
-    if (native_assets_api->dlopen_executable == nullptr) {
-      *error = OS::SCreate(/*use malloc*/ nullptr,
-                           "NativeAssetsApi::dlopen_executable not set.");
-      return nullptr;
-    }
-    NoActiveIsolateScope no_active_isolate_scope;
-    handle = native_assets_api->dlopen_executable(error);
-  } else {
-    RELEASE_ASSERT(asset_type.Equals(Symbols::process()));
-    if (native_assets_api->dlopen_process == nullptr) {
-      *error = OS::SCreate(/*use malloc*/ nullptr,
-                           "NativeAssetsApi::dlopen_process not set.");
-      return nullptr;
-    }
-    NoActiveIsolateScope no_active_isolate_scope;
-    handle = native_assets_api->dlopen_process(error);
   }
 
   if (*error != nullptr) {
@@ -426,8 +442,6 @@
                             uintptr_t args_n,
                             char** error) {
   Thread* thread = Thread::Current();
-  Zone* zone = thread->zone();
-
   // Resolver resolution.
   auto resolver = GetFfiNativeResolver(thread, asset);
   if (resolver != nullptr) {
@@ -437,10 +451,8 @@
   }
 
   // Native assets resolution.
-  const auto& asset_location =
-      Array::Handle(zone, GetAssetLocation(thread, asset));
-  if (!asset_location.IsNull()) {
-    void* asset_result = FfiResolveAsset(thread, asset_location, symbol, error);
+  void* asset_result = FfiResolveAsset(thread, asset, symbol, error);
+  if (asset_result != nullptr) {
     return reinterpret_cast<intptr_t>(asset_result);
   }
 
@@ -456,11 +468,23 @@
     // Process lookup failed, but the user might have tried to use native
     // asset lookup. So augment the error message to include native assets info.
     char* process_lookup_error = *error;
-    *error = OS::SCreate(/*use malloc*/ nullptr,
-                         "No asset with id '%s' found. %s "
-                         "Attempted to fallback to process lookup. %s",
-                         asset.ToCString(), AvailableAssetsToCString(thread),
-                         process_lookup_error);
+    NativeAssetsApi* native_assets_api =
+        thread->isolate_group()->native_assets_api();
+    const char* const format =
+        "No asset with id '%s' found. %s "
+        "Attempted to fallback to process lookup. %s";
+    if (native_assets_api->available_assets != nullptr) {
+      // Embedder is resolving asset ids to asset paths.
+      char* available_assets = native_assets_api->available_assets();
+      *error = OS::SCreate(/*use malloc*/ nullptr, format, asset.ToCString(),
+                           available_assets, process_lookup_error);
+      free(available_assets);
+    } else {
+      // VM is resolving asset ids to asset paths.
+      *error =
+          OS::SCreate(/*use malloc*/ nullptr, format, asset.ToCString(),
+                      AvailableAssetsToCString(thread), process_lookup_error);
+    }
     free(process_lookup_error);
   }