chromium/third_party/jni_zero/jni_zero.gni

# Copyright 2023 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import("//build/config/android/config.gni")
import("//build/config/compiler/compiler.gni")
if (is_android) {
  import("//build/config/android/rules.gni")
}

_JAVAP_PATH = "//third_party/jdk/current/bin/javap"

declare_args() {
  # Enables JNI multiplexing to reduce JNI native methods overhead. Component
  # build must be false as most components are not visible to the base component
  # which dispatches out to every component. Disabling for cronet, at least for
  # now, since it needs to be able to convert GN to Soong build rules, which we
  # don't want to do right now. use_hashed_jni_names is required until we can
  # determine method names (and namespaces) of all java->native functions.
  enable_jni_multiplexing =
      !is_java_debug && !is_component_build && !is_cronet_build
}

declare_args() {
  # Use hashed symbol names to reduce JNI symbol overhead when not multiplexing.
  use_hashed_jni_names = !enable_jni_multiplexing && !is_java_debug
}

assert(!use_hashed_jni_names || !enable_jni_multiplexing,
       "Multiplexing requires that hashed names be disabled.")

# Use a dedicated include dir so that files can #include headers from other
# toolchains without affecting non-JNI #includes.
if (target_os == "android") {
  jni_headers_dir = "$root_build_dir/gen/jni_headers"
} else {
  # Chrome OS builds cannot share gen/ directories because is_android=false
  # within default_toolchain.
  jni_headers_dir = "$root_gen_dir/jni_headers"
}

_jni_zero_dir = "//third_party/jni_zero"

template("jni_sources_list") {
  generated_file(target_name) {
    forward_variables_from(invoker,
                           TESTONLY_AND_VISIBILITY + [
                                 "deps",
                                 "walk_keys",
                               ])
    outputs = [ invoker.output ]
    data_keys = [ "jni_source_files" ]
    rebase = root_build_dir
    metadata = {
      # This target is just collecting source files used - this is not a
      # legitimate dependency.
      shared_libraries_barrier = []
    }
  }
}

template("_invoke_jni_zero") {
  action(target_name) {
    forward_variables_from(invoker, "*", TESTONLY_AND_VISIBILITY)
    forward_variables_from(invoker, TESTONLY_AND_VISIBILITY)

    script = "//third_party/jni_zero/jni_zero.py"
    if (!defined(inputs)) {
      inputs = []
    }
    inputs += rebase_path([
                            "codegen/called_by_native_header.py",
                            "codegen/convert_type.py",
                            "codegen/header_common.py",
                            "codegen/natives_header.py",
                            "codegen/placeholder_gen_jni_java.py",
                            "codegen/placeholder_java_type.py",
                            "codegen/proxy_impl_java.py",
                            "common.py",
                            "java_lang_classes.py",
                            "java_types.py",
                            "jni_generator.py",
                            "jni_registration_generator.py",
                            "jni_zero.py",
                            "parse.py",
                            "proxy.py",
                          ],
                          ".",
                          _jni_zero_dir)
  }
}

# Declare a jni registration target.
#
# This target generates a srcjar containing a copy of GEN_JNI.java, which has
# the native methods of all dependent java files. It can also create a .h file
# for use with manual JNI registration.
#
# The script does not scan any generated sources (those within .srcjars, or
# within root_build_dir). This could be fixed by adding deps & logic to scan
# .srcjars, but isn't currently needed.
#
# See third_party/jni_zero/jni_registration_generator.py for more info
# about the format of the header file.
#
# Variables
#   java_targets: List of android_* targets that comprise your app.
#   native_deps: List of shared_library targets that comprise your app.
#   manual_jni_registration: Manually do JNI registration - required for feature
#     splits which provide their own native library. (optional)
#   namespace: Registration functions will be wrapped into this. (optional)
#   require_native_mocks: Enforce that any native calls using
#     org.jni_zero.NativeMethods must have a mock set
#     (optional).
#   enable_native_mocks: Allow native calls using
#     org.jni_zero.NativeMethods to be mocked in tests
#     (optional).
#   priority_java_targets: List of java targets that, if using multiplexing,
#     will always be placed first in the sequential switch numbers. (optional)
#
# Example
#   generate_jni_registration("chrome_jni_registration") {
#     java_targets = [ ":chrome_public_apk" ]
#     manual_jni_registration = false
#   }
template("generate_jni_registration") {
  forward_variables_from(invoker, TESTONLY_AND_VISIBILITY)
  if (defined(invoker.native_deps)) {
    _native_sources_list = "$target_gen_dir/$target_name.nativesources.txt"
    jni_sources_list("${target_name}__native_sources") {
      deps = invoker.native_deps
      output = _native_sources_list
    }
  }

  _java_sources_list = "$target_gen_dir/$target_name.javasources.txt"
  jni_sources_list("${target_name}__java_sources") {
    deps = invoker.java_targets
    output = _java_sources_list

    # When apk or bundle module targets are uses, do not pull metadata from
    # their native library deps.
    walk_keys = [ "java_walk_keys" ]
  }

  if (defined(invoker.priority_java_targets)) {
    if (enable_jni_multiplexing) {
      # This is intended for WebView, so we can only look at Java sources. This
      # is becuase WebView's java is a strict subset of it's native, so we don't
      # need to take the intersection of it's native and java.
      _priority_java_sources_list =
          "$target_gen_dir/$target_name.priority_javasources.txt"
      jni_sources_list("${target_name}__priority_java_sources") {
        deps = invoker.priority_java_targets
        output = _priority_java_sources_list

        # When apk or bundle module targets are uses, do not pull metadata from
        # their native library deps.
        walk_keys = [ "java_walk_keys" ]
      }
    } else {
      not_needed(invoker, [ "priority_java_targets" ])
    }
  }

  _invoke_jni_zero(target_name) {
    # Cannot depend on jni_sources_list targets since they likely depend on
    # this target via srcjar_deps. Depfiles are used to add the dep instead.
    deps = []
    _srcjar_output = "$target_gen_dir/$target_name.srcjar"
    outputs = [ _srcjar_output ]
    depfile = "$target_gen_dir/$target_name.d"

    java_target_deps = []
    if (defined(invoker.java_targets)) {
      foreach(java_targets_dep, invoker.java_targets) {
        java_target_deps +=
            [ get_label_info(java_targets_dep, "label_no_toolchain") ]
      }
    }
    args = [
      "generate-final",
      "--srcjar-path",
      rebase_path(_srcjar_output, root_build_dir),
      "--depfile",
      rebase_path(depfile, root_build_dir),
      "--java-sources-file",
      rebase_path(_java_sources_list, root_build_dir),
    ]

    if (defined(_native_sources_list)) {
      args += [
        "--native-sources-file",
        rebase_path(_native_sources_list, root_build_dir),
      ]
    }

    if (defined(invoker.include_testonly)) {
      _include_testonly = invoker.include_testonly
    } else {
      _include_testonly = defined(testonly) && testonly
    }
    if (_include_testonly) {
      args += [ "--include-test-only" ]
    }

    if (use_hashed_jni_names) {
      args += [ "--use-proxy-hash" ]
    }

    if (defined(invoker.enable_native_mocks) && invoker.enable_native_mocks) {
      args += [ "--enable-proxy-mocks" ]

      if (defined(invoker.require_native_mocks) &&
          invoker.require_native_mocks) {
        args += [ "--require-mocks" ]
      }
    }

    if (defined(invoker.remove_uncalled_jni) && invoker.remove_uncalled_jni) {
      args += [ "--remove-uncalled-methods" ]
    }
    if (defined(invoker.add_stubs_for_missing_jni) &&
        invoker.add_stubs_for_missing_jni) {
      args += [ "--add-stubs-for-missing-native" ]
    }

    if (defined(invoker.cpp_codegen_output)) {
      _cpp_codegen_output = invoker.cpp_codegen_output
      outputs += [ _cpp_codegen_output ]
      args += [
        "--header-path",
        rebase_path(_cpp_codegen_output, root_build_dir),
      ]

      public_configs = [
        # This gives targets depending on this registration access to our
        # generated C++ file.
        "//third_party/jni_zero:jni_include_dir",
      ]
      if (defined(invoker.manual_jni_registration) &&
          invoker.manual_jni_registration) {
        args += [ "--manual-jni-registration" ]
      }
      if (enable_jni_multiplexing) {
        args += [ "--enable-jni-multiplexing" ]
      }
      if (defined(_priority_java_sources_list)) {
        args += [
          "--priority-java-sources-file",
          rebase_path(_priority_java_sources_list, root_build_dir),
        ]
      }
    }

    if (defined(invoker.namespace)) {
      args += [ "--namespace=${invoker.namespace}" ]
    }

    if (defined(invoker.module_name)) {
      args += [ "--module-name=${invoker.module_name}" ]
    }
  }
}

# JNI target implementation. See generate_jni or generate_jar_jni for usage.
template("generate_jni_impl") {
  public_configs = []

  # A hack to prevent GN from treating this dep as a java dep, since we depend
  # onto the invoke_jni_zero action from a java_library, which unfortunately
  # checks to see if a given dep is a java dep by searching for the strings
  # "java" or "junit".
  _target_name_without_java_or_junit =
      string_replace(string_replace(target_name, "_java", "_J"), "_junit", "_U")
  _jni_zero_action_target_name = _target_name_without_java_or_junit + "__action"
  if (current_toolchain != default_toolchain && target_os == "android") {
    # Rather than regenerating .h files in secondary toolchains, re-use the
    # ones from the primary toolchain by depending on it and adding the
    # root gen directory to the include paths.
    # https://crbug.com/1369398
    group(target_name) {
      forward_variables_from(invoker, TESTONLY_AND_VISIBILITY)
      not_needed(invoker, "*")
      public_configs +=
          [ "//third_party/jni_zero:jni_include_dir($default_toolchain)" ]

      # Depending on the action name to avoid cross-toolchain native deps.
      public_deps = [ ":$_jni_zero_action_target_name($default_toolchain)" ]
      metadata = {
        shared_libraries_barrier = []
      }
    }
  } else {
    _final_target_name = target_name
    if (defined(invoker.classes)) {
      _from_source = false
    } else {
      _from_source = true

      # Using final_target_name to make srcjar_deps work.
      _srcjar_output = "$target_gen_dir/$_final_target_name.srcjar"
      _placeholder_srcjar_output =
          "$target_gen_dir/${_final_target_name}_placeholder.srcjar"
    }

    _invoke_jni_zero(_jni_zero_action_target_name) {
      _subdir = rebase_path(target_gen_dir, root_gen_dir)
      _jni_output_dir = "$jni_headers_dir/$_subdir/$_final_target_name"
      if (defined(invoker.jni_generator_include)) {
        _jni_generator_include = invoker.jni_generator_include
      } else {
        _jni_generator_include = "//third_party/jni_zero/jni_zero_internal.h"
      }

      # The sources aren't compiled so don't check their dependencies.
      check_includes = false
      forward_variables_from(invoker,
                             [
                               "deps",
                               "metadata",
                               "public_deps",
                             ])
      if (!defined(public_deps)) {
        public_deps = []
      }
      public_configs += [ "//third_party/jni_zero:jni_include_dir" ]

      inputs = []
      outputs = []
      args = []
      if (_from_source) {
        args += [ "from-source" ]
      } else {
        args += [ "from-jar" ]
      }
      args += [
        "--output-dir",
        rebase_path(_jni_output_dir, root_build_dir),
        "--extra-include",
        rebase_path(_jni_generator_include, _jni_output_dir),
      ]

      if (_from_source) {
        assert(defined(invoker.sources))

        args += [
          "--srcjar-path",
          rebase_path(_srcjar_output, root_build_dir),
          "--placeholder-srcjar-path",
          rebase_path(_placeholder_srcjar_output, root_build_dir),
        ]
        outputs += [
          _srcjar_output,
          _placeholder_srcjar_output,
        ]
        inputs += invoker.sources
        _input_args = rebase_path(invoker.sources, root_build_dir)
        _input_names = invoker.sources
        if (use_hashed_jni_names) {
          args += [ "--use-proxy-hash" ]
        }

        if (enable_jni_multiplexing) {
          args += [ "--enable-jni-multiplexing" ]
        }
        if (defined(invoker.namespace)) {
          args += [ "--namespace=${invoker.namespace}" ]
        }
      } else {
        if (is_robolectric) {
          not_needed(invoker, [ "jar_file" ])
        } else {
          if (defined(invoker.jar_file)) {
            _jar_file = invoker.jar_file
          } else {
            _jar_file = android_sdk_jar
          }
          inputs += [
            _jar_file,
            _JAVAP_PATH,
          ]
          args += [
            "--jar-file",
            rebase_path(_jar_file, root_build_dir),
            "--javap",
            rebase_path(_JAVAP_PATH, root_build_dir),
          ]
        }
        _input_args = invoker.classes
        _input_names = invoker.classes
        if (defined(invoker.unchecked_exceptions) &&
            invoker.unchecked_exceptions) {
          args += [ "--unchecked-exceptions" ]
        }
      }

      if (defined(invoker.split_name)) {
        args += [ "--split-name=${invoker.split_name}" ]
      }

      foreach(_name, _input_names) {
        _name =
            string_replace(get_path_info(_name, "name"), "\$", "__") + "_jni.h"
        outputs += [ "$_jni_output_dir/$_name" ]

        # Avoid passing GN lists because not all webrtc embedders use //build.
        args += [
          "--output-name",
          _name,
        ]
      }

      foreach(_input, _input_args) {
        args += [ "--input-file=$_input" ]
      }
    }

    if (_from_source) {
      java_library("${_final_target_name}_java") {
        forward_variables_from(invoker, TESTONLY_AND_VISIBILITY)
        requires_android = true
        srcjars = [
          _srcjar_output,
          _placeholder_srcjar_output,
        ]
        supports_android = true
        jar_included_patterns = [
          "*Jni.class",
          "*Jni\$*.class",
        ]
        prevent_excluded_classes_from_classpath = true
        deps = [
          ":$_jni_zero_action_target_name",
          "//third_party/jni_zero:gendeps_java",
        ]
      }
    }

    # This group exists to allow for users of generate_jni() to get our object
    # files included in their executables without explicitly depending on our
    # targets in jni_zero/BUILD.gn.
    group(_final_target_name) {
      public_deps = [ ":$_jni_zero_action_target_name" ]
      forward_variables_from(invoker, TESTONLY_AND_VISIBILITY)
      if (defined(visibility)) {
        visibility += [ ":$target_name" ]
      }
    }
  }
}

# Declare a jni target
#
# This target generates the native jni bindings for a set of .java files.
#
# See third_party/jni_zero/jni_generator.py for more info about the
# format of generating JNI bindings.
#
# Variables
#   sources: list of .java files to generate jni for
#   namespace: Specify the namespace for the generated header file.
#   deps, public_deps: As normal
#
# Example
#   # Target located in base/BUILD.gn.
#   generate_jni("foo_jni") {
#     # Generates gen/base/foo_jni/Foo_jni.h
#     # To use: #include "base/foo_jni/Foo_jni.h"
#     sources = [
#       "android/java/src/org/chromium/foo/Foo.java",
#       ...,
#     ]
#   }
template("generate_jni") {
  generate_jni_impl(target_name) {
    forward_variables_from(invoker, "*", TESTONLY_AND_VISIBILITY)
    forward_variables_from(invoker, TESTONLY_AND_VISIBILITY)
    metadata = {
      # keep as abspath as this is used by cronet team
      # to convert JNI registration targets from GN to BP (GN2BP).
      jni_source_files = get_path_info(sources, "abspath")
    }
  }
}

# Declare a jni target for a prebuilt jar
#
# This target generates the native jni bindings for a set of classes in a .jar.
#
# See third_party/jni_zero/jni_generator.py for more info about the
# format of generating JNI bindings.
#
# Variables
#   classes: list of .class files in the jar to generate jni for. These should
#     include the full path to the .class file.
#   jar_file: the path to the .jar. If not provided, will default to the sdk's
#     android.jar
#   unchecked_exceptions: Don't CHECK() for exceptions in generated stubs.
#     This behaves as if every method had @CalledByNativeUnchecked.
#   deps, public_deps: As normal
#
# Example
#   # Target located in base/BUILD.gn.
#   generate_jar_jni("foo_jni") {
#     # Generates gen/base/foo_jni/Runnable_jni.h
#     # To use: #include "base/foo_jni/Runnable_jni.h"
#     classes = [
#       "android/view/Foo.class",
#     ]
#   }
template("generate_jar_jni") {
  generate_jni_impl(target_name) {
    forward_variables_from(invoker, "*", TESTONLY_AND_VISIBILITY)
    forward_variables_from(invoker, TESTONLY_AND_VISIBILITY)
  }
}

# This is a wrapper around an underlying native target which inserts JNI
# registration.
#
# The registration is based on the closure of the native target's generate_jni
# transitive dependencies. Additionally, we use provided java_targets to assert
# that our native and Java sides line up.
#
# In order to depend on the JNI registration, use
# <native-target-name>__jni_registration.
template("native_with_jni") {
  _enable_underlying_native =
      !defined(invoker.enable_target) || invoker.enable_target
  _manual_jni_registration = defined(invoker.manual_jni_registration) &&
                             invoker.manual_jni_registration
  _needs_cpp_codegen =
      (_manual_jni_registration || enable_jni_multiplexing) &&
      !(defined(invoker.collect_inputs_only) && invoker.collect_inputs_only)
  _needs_native_dep = _enable_underlying_native && _needs_cpp_codegen
  if ((_needs_cpp_codegen && current_toolchain == default_toolchain) ||
      _needs_native_dep) {
    _subdir = rebase_path(target_gen_dir, root_gen_dir)
    _registration_cpp_codegen_output =
        "$jni_headers_dir/$_subdir/${target_name}__jni_registration_generated"

    # Make it a header for jni_registration (where we need to #include it) and
    # .cc when it's multiplexing.
    if (_manual_jni_registration) {
      _registration_cpp_codegen_output += ".h"
    } else {
      _registration_cpp_codegen_output += ".cc"
    }
  }
  if (_needs_native_dep || current_toolchain == default_toolchain) {
    _jni_registration_target_name = "${target_name}__jni_registration"
  }

  if (current_toolchain == default_toolchain) {
    if (defined(invoker.visibility)) {
      _target_name_for_visibility = target_name
    }
    generate_jni_registration(_jni_registration_target_name) {
      forward_variables_from(invoker, TESTONLY_AND_VISIBILITY)
      if (defined(visibility)) {
        visibility += [ ":$_target_name_for_visibility" ]
      }
      native_deps = invoker.deps

      if (defined(invoker.testonly) && invoker.testonly) {
        enable_native_mocks = true
        if (!defined(invoker.add_stubs_for_missing_jni)) {
          add_stubs_for_missing_jni = true
        }
        if (!defined(invoker.remove_uncalled_jni)) {
          remove_uncalled_jni = true
        }
      }
      if (_needs_cpp_codegen) {
        cpp_codegen_output = _registration_cpp_codegen_output
      }
      forward_variables_from(invoker,
                             [
                               "add_stubs_for_missing_jni",
                               "java_targets",
                               "manual_jni_registration",
                               "module_name",
                               "namespace",
                               "remove_uncalled_jni",
                               "priority_java_targets",
                             ])
    }
  } else {
    not_needed(invoker,
               [
                 "add_stubs_for_missing_jni",
                 "java_targets",
                 "manual_jni_registration",
                 "module_name",
                 "namespace",
                 "remove_uncalled_jni",
                 "priority_java_targets",
               ])
  }

  if (_enable_underlying_native) {
    if (defined(invoker.target_type_import)) {
      import(invoker.target_type_import)
    }
    target(invoker.target_type, target_name) {
      deps = invoker.deps
      if (defined(invoker.sources)) {
        sources = invoker.sources
      }

      # Need to overwrite configs, which have defaults. We assume we have
      # already set the correct defaults in the invoker.
      configs = []
      configs = invoker.configs
      if (_needs_native_dep) {
        configs +=
            [ "//third_party/jni_zero:jni_include_dir($default_toolchain)" ]
        if (is_robolectric &&
            filter_include(configs, [ "//third_party/jdk" ]) == []) {
          # Adding jdk config if not already included - that's what the
          # filter_include is for.
          configs += [ "//third_party/jdk" ]
        }
        deps += [ ":$_jni_registration_target_name($default_toolchain)" ]
        if (!defined(sources)) {
          sources = []
        }
        sources += [ _registration_cpp_codegen_output ]
      }
      forward_variables_from(invoker, TESTONLY_AND_VISIBILITY)
      forward_variables_from(invoker,
                             "*",
                             TESTONLY_AND_VISIBILITY + [
                                   "configs",
                                   "deps",
                                   "sources",
                                 ])
    }
  } else {
    not_needed(invoker, "*")
    if (current_toolchain != default_toolchain) {
      not_needed([ "target_name" ])
    }
  }
}

# native_with_jni for shared libraries - see native_with_jni for details.
template("shared_library_with_jni") {
  native_with_jni(target_name) {
    forward_variables_from(invoker, "*", TESTONLY_AND_VISIBILITY)
    forward_variables_from(invoker, TESTONLY_AND_VISIBILITY)
    target_type = "shared_library"
  }
}
set_defaults("shared_library_with_jni") {
  configs = default_shared_library_configs
}

# native_with_jni for components - see native_with_jni for details.
template("component_with_jni") {
  native_with_jni(target_name) {
    forward_variables_from(invoker, "*", TESTONLY_AND_VISIBILITY)
    forward_variables_from(invoker, TESTONLY_AND_VISIBILITY)
    target_type = "component"
  }
}
set_defaults("component_with_jni") {
  configs = default_component_configs
}