Auto-generate Java that provide information about modules at runtime

The first piece of information is the native libraries a module depends
on. We will use this info in following CLs to auto-load the libraries
on first module access.

The Java is generated in the chrome_feature_module template via a new
module_desc_java template. Module_desc_java lives in
//components/module_installer since this component is using the Java and
has expectations about its format.

Modules that don't use module descriptors (e.g. they are packaged into
base such as tab_management in this CL) have to manually create a
module_desc_java to be able to use Module.java's loading goodies.

APKs won't automatically have module_desc_javas. In order to make the
Module.java API work in APKs (e.g. test APKs) we use stub descriptors.
This should be fine as APKs already load all native libraries at
startup.

See go/native-dfm-load-v2 for more context and details.

Also in this CL, moving native lib loading before impl initialization
so that the module is fully set up by the time module code is handed
control.

Bug: 870055
Change-Id: I8b6112bfc57b7d49698702b4fc12d2874b55fa12
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1873911
Commit-Queue: Tibor Goldschwendt <[email protected]>
Commit-Queue: Christopher Grant <[email protected]>
Auto-Submit: Tibor Goldschwendt <[email protected]>
Reviewed-by: Christopher Grant <[email protected]>
Reviewed-by: Andrew Grieve <[email protected]>
Cr-Commit-Position: refs/heads/master@{#709165}
diff --git a/PRESUBMIT.py b/PRESUBMIT.py
index e67a8c2..8de4ce3 100644
--- a/PRESUBMIT.py
+++ b/PRESUBMIT.py
@@ -1351,6 +1351,7 @@
     'build/android/test_wrapper/logdog_wrapper.pydeps',
     'build/protoc_java.pydeps',
     'chrome/android/features/create_stripped_java_factory.pydeps',
+    'components/module_installer/android/module_desc_java.pydeps',
     'net/tools/testserver/testserver.pydeps',
     'testing/scripts/run_android_wpt.pydeps',
     'third_party/android_platform/development/scripts/stack.pydeps',
diff --git a/chrome/android/BUILD.gn b/chrome/android/BUILD.gn
index 9f85b9aa5..5f08a28 100644
--- a/chrome/android/BUILD.gn
+++ b/chrome/android/BUILD.gn
@@ -508,7 +508,10 @@
   }
 
   if (disable_tab_ui_dfm) {
-    deps += [ "//chrome/android/features/tab_ui:java" ]
+    deps += [
+      "//chrome/android/features/tab_ui:java",
+      "//chrome/android/features/tab_ui:module_desc_java",
+    ]
   }
 
   if (dfmify_dev_ui) {
diff --git a/chrome/android/features/tab_ui/BUILD.gn b/chrome/android/features/tab_ui/BUILD.gn
index 79f33bd..d761d60 100644
--- a/chrome/android/features/tab_ui/BUILD.gn
+++ b/chrome/android/features/tab_ui/BUILD.gn
@@ -5,6 +5,7 @@
 import("//build/config/android/rules.gni")
 import("//chrome/android/features/tab_ui/buildflags.gni")
 import("//chrome/common/features.gni")
+import("//components/module_installer/android/module_desc_java.gni")
 
 java_strings_grd("java_strings_grd") {
   defines = chrome_grit_defines
@@ -180,3 +181,7 @@
     "//ui/android:ui_java",
   ]
 }
+
+module_desc_java("module_desc_java") {
+  module_name = "tab_management"
+}
diff --git a/chrome/android/modules/chrome_feature_module_tmpl.gni b/chrome/android/modules/chrome_feature_module_tmpl.gni
index cfc20859..b708267 100644
--- a/chrome/android/modules/chrome_feature_module_tmpl.gni
+++ b/chrome/android/modules/chrome_feature_module_tmpl.gni
@@ -5,6 +5,7 @@
 import("//build/config/android/rules.gni")
 import("//build/config/locales.gni")
 import("//chrome/android/modules/chrome_feature_modules.gni")
+import("//components/module_installer/android/module_desc_java.gni")
 
 # Instantiates a Chrome-specific app bundle module.
 #
@@ -27,6 +28,55 @@
   _include_32_bit_webview =
       defined(invoker.include_32_bit_webview) && invoker.include_32_bit_webview
 
+  _loadable_modules_32_bit = []
+  if (defined(_module_desc.loadable_modules_32_bit)) {
+    _loadable_modules_32_bit += _module_desc.loadable_modules_32_bit
+  }
+
+  _loadable_modules_64_bit = []
+  if (defined(_module_desc.loadable_modules_64_bit)) {
+    _loadable_modules_64_bit += _module_desc.loadable_modules_64_bit
+  }
+
+  _shared_libraries = []
+  if (use_native_partitions && defined(_module_desc.native_deps) &&
+      _module_desc.native_deps != []) {
+    _arch = ""
+    _toolchain = ""
+    _root_out_dir = root_out_dir
+    if (android_64bit_target_cpu && _is_monochrome_or_trichrome) {
+      if (_is_64_bit_browser) {
+        _arch = "_64"
+      } else {
+        _toolchain = "($android_secondary_abi_toolchain)"
+        _root_out_dir = get_label_info(":foo($android_secondary_abi_toolchain)",
+                                       "root_out_dir")
+      }
+    }
+    if (_is_monochrome_or_trichrome) {
+      _base_target_name = "libmonochrome${_arch}"
+    } else {
+      _base_target_name = "libchrome${_arch}"
+    }
+    _shared_libraries += [
+      "//chrome/android:${_base_target_name}_${_module_desc.name}${_toolchain}",
+    ]
+    _native_library = "${_root_out_dir}/${_base_target_name}_partitions/lib${_module_desc.name}.so"
+
+    # Pass the correct library as both the 32 and 64-bit options. Underlying
+    # logic will choose from the correct variable, and supply a dummy library
+    # for the other architecture if required.
+    _loadable_modules_32_bit += [ _native_library ]
+    _loadable_modules_64_bit += [ _native_library ]
+  } else {
+    not_needed([ "_is_monochrome_or_trichrome" ])
+  }
+
+  module_desc_java("${target_name}__module_desc_java") {
+    module_name = _module_desc.name
+    shared_libraries = _shared_libraries
+  }
+
   android_app_bundle_module(target_name) {
     forward_variables_from(invoker,
                            [
@@ -40,59 +90,18 @@
                            ])
     android_manifest = _module_desc.android_manifest
     target_sdk_version = android_sdk_version
-    deps = []
+    deps = [
+      ":${target_name}__module_desc_java",
+    ]
     if (defined(_module_desc.java_deps)) {
       deps += _module_desc.java_deps
     }
 
     # Don't embed more translations than required (http://crbug.com/932017).
     aapt_locale_whitelist = locales
-
     proguard_enabled = !is_java_debug
-
     package_name = _module_desc.name
 
-    _loadable_modules_32_bit = []
-    _loadable_modules_64_bit = []
-    if (defined(_module_desc.loadable_modules_32_bit)) {
-      _loadable_modules_32_bit += _module_desc.loadable_modules_32_bit
-    }
-    if (defined(_module_desc.loadable_modules_64_bit)) {
-      _loadable_modules_64_bit += _module_desc.loadable_modules_64_bit
-    }
-
-    if (use_native_partitions && defined(_module_desc.native_deps) &&
-        _module_desc.native_deps != []) {
-      _arch = ""
-      _toolchain = ""
-      _root_out_dir = root_out_dir
-      if (android_64bit_target_cpu && _is_monochrome_or_trichrome) {
-        if (_is_64_bit_browser) {
-          _arch = "_64"
-        } else {
-          _toolchain = "($android_secondary_abi_toolchain)"
-          _root_out_dir =
-              get_label_info(":foo($android_secondary_abi_toolchain)",
-                             "root_out_dir")
-        }
-      }
-      if (_is_monochrome_or_trichrome) {
-        _base_target_name = "libmonochrome${_arch}"
-      } else {
-        _base_target_name = "libchrome${_arch}"
-      }
-      deps += [ "//chrome/android:${_base_target_name}_${_module_desc.name}${_toolchain}" ]
-      _native_library = "${_root_out_dir}/${_base_target_name}_partitions/lib${_module_desc.name}.so"
-
-      # Pass the correct library as both the 32 and 64-bit options. Underlying
-      # logic will choose from the correct variable, and supply a dummy library
-      # for the other architecture if required.
-      _loadable_modules_32_bit += [ _native_library ]
-      _loadable_modules_64_bit += [ _native_library ]
-    } else {
-      not_needed([ "_is_monochrome_or_trichrome" ])
-    }
-
     # Specify native libraries and placeholders.
     if (_loadable_modules_32_bit != [] || _loadable_modules_64_bit != []) {
       # Decision logic: Assign decision variables:
diff --git a/components/module_installer/android/BUILD.gn b/components/module_installer/android/BUILD.gn
index 8a6f396..07dfc24 100644
--- a/components/module_installer/android/BUILD.gn
+++ b/components/module_installer/android/BUILD.gn
@@ -8,11 +8,12 @@
 android_library("module_installer_java") {
   java_files = [
     "java/src/org/chromium/components/module_installer/builder/Module.java",
+    "java/src/org/chromium/components/module_installer/builder/ModuleDescriptor.java",
     "java/src/org/chromium/components/module_installer/builder/ModuleEngine.java",
     "java/src/org/chromium/components/module_installer/engine/ApkEngine.java",
+    "java/src/org/chromium/components/module_installer/engine/EngineFactory.java",
     "java/src/org/chromium/components/module_installer/engine/FakeEngine.java",
     "java/src/org/chromium/components/module_installer/engine/InstallEngine.java",
-    "java/src/org/chromium/components/module_installer/engine/EngineFactory.java",
     "java/src/org/chromium/components/module_installer/engine/InstallListener.java",
     "java/src/org/chromium/components/module_installer/engine/SplitCompatEngine.java",
     "java/src/org/chromium/components/module_installer/engine/SplitCompatEngineFacade.java",
diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/builder/Module.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/builder/Module.java
index fdeaba2..c27bd09 100644
--- a/components/module_installer/android/java/src/org/chromium/components/module_installer/builder/Module.java
+++ b/components/module_installer/android/java/src/org/chromium/components/module_installer/builder/Module.java
@@ -4,6 +4,7 @@
 
 package org.chromium.components.module_installer.builder;
 
+import org.chromium.base.BuildConfig;
 import org.chromium.base.StrictModeContext;
 import org.chromium.base.VisibleForTesting;
 import org.chromium.base.annotations.JNINamespace;
@@ -113,15 +114,6 @@
             if (mImpl != null) return mImpl;
 
             assert isInstalled();
-            // Accessing classes in the module may cause its DEX file to be loaded. And on some
-            // devices that causes a read mode violation.
-            try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
-                mImpl = mInterfaceClass.cast(Class.forName(mImplClassName).newInstance());
-            } catch (ClassNotFoundException | InstantiationException | IllegalAccessException
-                    | IllegalArgumentException e) {
-                throw new RuntimeException(e);
-            }
-
             // Load the module's native code and/or resources if they are present, and the Chrome
             // native library itself has been loaded.
             if (sPendingNativeRegistrations == null) {
@@ -132,6 +124,7 @@
                 // initialization.
                 sPendingNativeRegistrations.add(mName);
             }
+            mImpl = mInterfaceClass.cast(instantiateReflectively(mImplClassName));
             return mImpl;
         }
     }
@@ -139,8 +132,11 @@
     private static void loadNative(String name) {
         // Can only initialize native once per lifetime of Chrome.
         if (sInitializedModules.contains(name)) return;
-        // TODO(crbug.com/870055, crbug.com/986960): Automatically determine if module has native
-        // code or resources instead of whitelisting.
+        // TODO(crbug.com/870055): Use |libraries| instead of whitelist to load
+        // native libraries.
+        String[] libraries = loadModuleDescriptor(name).getLibraries();
+        // TODO(crbug.com/986960): Automatically determine if module has native
+        // resources instead of whitelisting.
         boolean loadLibrary = false;
         boolean loadResources = false;
         if ("test_dummy".equals(name)) {
@@ -156,6 +152,47 @@
         sInitializedModules.add(name);
     }
 
+    /**
+     * Loads the {@link ModuleDescriptor} for a module.
+     *
+     * For bundles, uses reflection to load the descriptor from inside the
+     * module. For APKs, returns an empty descriptor since APKs won't have
+     * descriptors packaged into them.
+     *
+     * @param name The module's name.
+     * @return The module's {@link ModuleDescriptor}.
+     */
+    private static ModuleDescriptor loadModuleDescriptor(String name) {
+        if (!BuildConfig.IS_BUNDLE) {
+            return new ModuleDescriptor() {
+                @Override
+                public String[] getLibraries() {
+                    return new String[0];
+                }
+            };
+        }
+
+        return (ModuleDescriptor) instantiateReflectively(
+                "org.chromium.components.module_installer.builder.ModuleDescriptor_" + name);
+    }
+
+    /**
+     * Instantiates an object via reflection.
+     *
+     * Ignores strict mode violations since accessing code in a module may cause its DEX file to be
+     * loaded and on some devices that can cause such a violation.
+     *
+     * @param className The object's class name.
+     * @return The object.
+     */
+    private static Object instantiateReflectively(String className) {
+        try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
+            return Class.forName(className).newInstance();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
     @NativeMethods
     interface Natives {
         void loadNative(String name, boolean loadLibrary, boolean loadResources);
diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/builder/ModuleDescriptor.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/builder/ModuleDescriptor.java
new file mode 100644
index 0000000..7c8f941
--- /dev/null
+++ b/components/module_installer/android/java/src/org/chromium/components/module_installer/builder/ModuleDescriptor.java
@@ -0,0 +1,15 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.components.module_installer.builder;
+
+/**
+ * Provides information about a dynamic feature module.
+ */
+public interface ModuleDescriptor {
+    /**
+     * Returns the list of native library names this module requires at runtime.
+     */
+    String[] getLibraries();
+}
diff --git a/components/module_installer/android/module_desc_java.gni b/components/module_installer/android/module_desc_java.gni
new file mode 100644
index 0000000..293a032e
--- /dev/null
+++ b/components/module_installer/android/module_desc_java.gni
@@ -0,0 +1,67 @@
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/config/android/rules.gni")
+import("//build/config/python.gni")
+
+# Writes an implementation of
+# |org.chromium.components.module_installer.builder.ModuleDescriptor| for a
+# particular module to Java. The module loader backend expects such an
+# implementation for each module to automate module setup on first access.
+# Instantiations of this template can be depended on like |android_library|
+# targets.
+#
+# Supports the following variables:
+#   module_name: Name of the module.
+#   shared_libraries: (Optional) List of shared_library targets the module
+#     requires at runtime. Will consider all transitively depended on
+#     shared_libraries.
+template("module_desc_java") {
+  _target_name = target_name
+
+  _libraries = "${target_gen_dir}/${_target_name}.libraries"
+  _rebased_libraries = rebase_path(_libraries, root_out_dir)
+  generated_file("${_target_name}__libraries") {
+    if (defined(invoker.shared_libraries)) {
+      deps = invoker.shared_libraries
+    }
+    outputs = [
+      _libraries,
+    ]
+    data_keys = [ "shared_libraries" ]
+    walk_keys = [ "shared_libraries_barrier" ]
+    rebase = root_build_dir
+    output_conversion = "json"
+  }
+
+  _srcjar = "$target_gen_dir/${_target_name}__srcjar.srcjar"
+  action_with_pydeps("${_target_name}__srcjar") {
+    script = "//components/module_installer/android/module_desc_java.py"
+    deps = [
+      ":${_target_name}__libraries",
+    ]
+    inputs = [
+      _libraries,
+    ]
+    outputs = [
+      _srcjar,
+    ]
+    args = [
+      "--module",
+      invoker.module_name,
+      "--libraries",
+      "@FileArg($_rebased_libraries)",
+      "--output",
+      rebase_path(_srcjar, root_out_dir),
+    ]
+  }
+
+  android_library(_target_name) {
+    deps = [
+      "//base:base_java",
+      "//components/module_installer/android:module_installer_java",
+    ]
+    srcjar_deps = [ ":${_target_name}__srcjar" ]
+  }
+}
diff --git a/components/module_installer/android/module_desc_java.py b/components/module_installer/android/module_desc_java.py
new file mode 100755
index 0000000..4082fc7782
--- /dev/null
+++ b/components/module_installer/android/module_desc_java.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python
+#
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Writes Java module descriptor to srcjar file."""
+
+import argparse
+import os
+import sys
+import zipfile
+
+sys.path.append(
+    os.path.join(
+        os.path.dirname(__file__), '..', '..', '..', 'build', 'android', 'gyp'))
+from util import build_utils
+
+_TEMPLATE = '''\
+// This file is autogenerated by
+//     components/module_installer/android/module_desc_java.py
+// Please do not change its content.
+
+package org.chromium.components.module_installer.builder;
+
+import org.chromium.base.annotations.UsedByReflection;
+
+@UsedByReflection("Module.java")
+public class ModuleDescriptor_{MODULE} implements ModuleDescriptor {{
+    private static final String[] LIBRARIES = {{{LIBRARIES}}};
+
+    @Override
+    public String[] getLibraries() {{
+        return LIBRARIES;
+    }}
+}}
+'''
+
+
+def main():
+  parser = argparse.ArgumentParser()
+  parser.add_argument('--module', required=True, help='The module name.')
+  parser.add_argument(
+      '--libraries', required=True, help='GN list of native library paths.')
+  parser.add_argument(
+      '--output', required=True, help='Path to the generated srcjar file.')
+  options = parser.parse_args(build_utils.ExpandFileArgs(sys.argv[1:]))
+  options.libraries = build_utils.ParseGnList(options.libraries)
+
+  libraries = []
+  for path in options.libraries:
+    path = path.strip()
+    filename = os.path.split(path)[1]
+    assert filename.startswith('lib')
+    assert filename.endswith('.so')
+    # Remove lib prefix and .so suffix.
+    libraries += [filename[3:-3]]
+
+  format_dict = {
+      'MODULE': options.module,
+      'LIBRARIES': ','.join(['"%s"' % l for l in libraries]),
+  }
+  with build_utils.AtomicOutput(options.output) as f:
+    with zipfile.ZipFile(f.name, 'w') as srcjar_file:
+      build_utils.AddToZipHermetic(
+          srcjar_file,
+          'org/chromium/components/module_installer/builder/'
+          'ModuleDescriptor_%s.java' % options.module,
+          data=_TEMPLATE.format(**format_dict))
+
+
+if __name__ == '__main__':
+  sys.exit(main())
diff --git a/components/module_installer/android/module_desc_java.pydeps b/components/module_installer/android/module_desc_java.pydeps
new file mode 100644
index 0000000..d87ae66
--- /dev/null
+++ b/components/module_installer/android/module_desc_java.pydeps
@@ -0,0 +1,7 @@
+# Generated by running:
+#   build/print_python_deps.py --root components/module_installer/android --output components/module_installer/android/module_desc_java.pydeps components/module_installer/android/module_desc_java.py
+../../../build/android/gyp/util/__init__.py
+../../../build/android/gyp/util/build_utils.py
+../../../build/android/gyp/util/md5_check.py
+../../../build/gn_helpers.py
+module_desc_java.py