0

Android: add java_cpp_features GN rule

This adds the java_cpp_features GN rule to autogenerate Java String
constants representing C++ features. This refactors parts of
java_cpp_strings so java_cpp_features can share it.

This aims to address the most common syntaxes for declaring C++
features (string literal names, "brace" and "equals brace"
initialization, with & without the "base::" namespace).

Design: http://go/autogen-java-features
Bug: 1060097
Fixed: 1091031
Test: vpython build/android/gpy/java_cpp_strings_tests.py
Test: vpython build/android/gpy/java_cpp_features_tests.py
Test: tools/md_browser/md_browser.py docs/android_accessing_cpp_features_in_java.md
Change-Id: I5311f72f8837c122186148cf183f4219087df96a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2412840
Auto-Submit: Nate Fischer <ntfschr@chromium.org>
Reviewed-by: Andrew Grieve <agrieve@chromium.org>
Reviewed-by: Henrique Nakashima <hnakashima@chromium.org>
Reviewed-by: Peter Wen <wnwen@chromium.org>
Commit-Queue: Andrew Grieve <agrieve@chromium.org>
Cr-Commit-Position: refs/heads/master@{#812889}
This commit is contained in:
Nate Fischer
2020-10-01 20:20:14 +00:00
committed by Commit Bot
parent 1c55ded397
commit ac07b26220
12 changed files with 771 additions and 148 deletions

@ -1345,6 +1345,7 @@ _GENERIC_PYDEPS_FILES = [
'build/android/gyp/ijar.pydeps',
'build/android/gyp/jacoco_instr.pydeps',
'build/android/gyp/java_cpp_enum.pydeps',
'build/android/gyp/java_cpp_features.pydeps',
'build/android/gyp/java_cpp_strings.pydeps',
'build/android/gyp/jetify_jar.pydeps',
'build/android/gyp/jinja_template.pydeps',

@ -333,9 +333,8 @@ def DoGenerate(source_paths):
'"// GENERATED_JAVA_ENUM_PACKAGE: foo"?' %
source_path)
for enum_definition in enum_definitions:
package_path = enum_definition.enum_package.replace('.', os.path.sep)
file_name = enum_definition.class_name + '.java'
output_path = os.path.join(package_path, file_name)
output_path = java_cpp_utils.GetJavaFilePath(enum_definition.enum_package,
enum_definition.class_name)
output = GenerateOutput(source_path, enum_definition)
yield output_path, output

@ -0,0 +1,110 @@
#!/user/bin/env python
#
# Copyright 2020 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 argparse
import os
import re
import sys
import zipfile
from util import build_utils
from util import java_cpp_utils
class FeatureParserDelegate(java_cpp_utils.CppConstantParser.Delegate):
# Ex. 'const base::Feature kConstantName{"StringNameOfTheFeature", ...};'
# would parse as:
# ExtractConstantName() -> 'ConstantName'
# ExtractValue() -> '"StringNameOfTheFeature"'
FEATURE_RE = re.compile(r'\s*const (?:base::)?Feature\s+k(\w+)\s*(?:=\s*)?{')
VALUE_RE = re.compile(r'\s*("(?:\"|[^"])*")\s*,')
def ExtractConstantName(self, line):
match = FeatureParserDelegate.FEATURE_RE.match(line)
return match.group(1) if match else None
def ExtractValue(self, line):
match = FeatureParserDelegate.VALUE_RE.search(line)
return match.group(1) if match else None
def CreateJavaConstant(self, name, value, comments):
return java_cpp_utils.JavaString(name, value, comments)
def _GenerateOutput(template, source_paths, template_path, features):
description_template = """
// This following string constants were inserted by
// {SCRIPT_NAME}
// From
// {SOURCE_PATHS}
// Into
// {TEMPLATE_PATH}
"""
values = {
'SCRIPT_NAME': java_cpp_utils.GetScriptName(),
'SOURCE_PATHS': ',\n // '.join(source_paths),
'TEMPLATE_PATH': template_path,
}
description = description_template.format(**values)
native_features = '\n\n'.join(x.Format() for x in features)
values = {
'NATIVE_FEATURES': description + native_features,
}
return template.format(**values)
def _ParseFeatureFile(path):
with open(path) as f:
feature_file_parser = java_cpp_utils.CppConstantParser(
FeatureParserDelegate(), f.readlines())
return feature_file_parser.Parse()
def _Generate(source_paths, template_path):
with open(template_path) as f:
lines = f.readlines()
template = ''.join(lines)
package, class_name = java_cpp_utils.ParseTemplateFile(lines)
output_path = java_cpp_utils.GetJavaFilePath(package, class_name)
features = []
for source_path in source_paths:
features.extend(_ParseFeatureFile(source_path))
output = _GenerateOutput(template, source_paths, template_path, features)
return output, output_path
def _Main(argv):
parser = argparse.ArgumentParser()
parser.add_argument('--srcjar',
required=True,
help='The path at which to generate the .srcjar file')
parser.add_argument('--template',
required=True,
help='The template file with which to generate the Java '
'class. Must have "{NATIVE_FEATURES}" somewhere in '
'the template.')
parser.add_argument('inputs',
nargs='+',
help='Input file(s)',
metavar='INPUTFILE')
args = parser.parse_args(argv)
with build_utils.AtomicOutput(args.srcjar) as f:
with zipfile.ZipFile(f, 'w', zipfile.ZIP_STORED) as srcjar:
data, path = _Generate(args.inputs, args.template)
build_utils.AddToZipHermetic(srcjar, path, data=data)
if __name__ == '__main__':
_Main(sys.argv[1:])

@ -0,0 +1,7 @@
# Generated by running:
# build/print_python_deps.py --root build/android/gyp --output build/android/gyp/java_cpp_features.pydeps build/android/gyp/java_cpp_features.py
../../gn_helpers.py
java_cpp_features.py
util/__init__.py
util/build_utils.py
util/java_cpp_utils.py

@ -0,0 +1,198 @@
#!/usr/bin/env python
# Copyright 2020 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.
"""Tests for java_cpp_features.py.
This test suite contains various tests for the C++ -> Java base::Feature
generator.
"""
import unittest
import java_cpp_features
from util import java_cpp_utils
class _TestFeaturesParser(unittest.TestCase):
def testParseComments(self):
test_data = """
/**
* This should be ignored as well.
*/
// Comment followed by a blank line.
// Comment followed by unrelated code.
int foo() { return 3; }
// Real comment.
const base::Feature kSomeFeature{"SomeFeature",
base::FEATURE_DISABLED_BY_DEFAULT};
// Real comment that spans
// multiple lines.
const base::Feature kSomeOtherFeature{"SomeOtherFeature",
base::FEATURE_ENABLED_BY_DEFAULT};
// Comment followed by nothing.
""".split('\n')
feature_file_parser = java_cpp_utils.CppConstantParser(
java_cpp_features.FeatureParserDelegate(), test_data)
features = feature_file_parser.Parse()
self.assertEqual(2, len(features))
self.assertEqual('SOME_FEATURE', features[0].name)
self.assertEqual('"SomeFeature"', features[0].value)
self.assertEqual(1, len(features[0].comments.split('\n')))
self.assertEqual('SOME_OTHER_FEATURE', features[1].name)
self.assertEqual('"SomeOtherFeature"', features[1].value)
self.assertEqual(2, len(features[1].comments.split('\n')))
def testWhitespace(self):
test_data = """
// 1 line
const base::Feature kShort{"Short", base::FEATURE_DISABLED_BY_DEFAULT};
// 2 lines
const base::Feature kTwoLineFeatureA{"TwoLineFeatureA",
base::FEATURE_DISABLED_BY_DEFAULT};
const base::Feature kTwoLineFeatureB{
"TwoLineFeatureB", base::FEATURE_DISABLED_BY_DEFAULT};
// 3 lines
const base::Feature kFeatureWithAVeryLongNameThatWillHaveToWrap{
"FeatureWithAVeryLongNameThatWillHaveToWrap",
base::FEATURE_DISABLED_BY_DEFAULT};
""".split('\n')
feature_file_parser = java_cpp_utils.CppConstantParser(
java_cpp_features.FeatureParserDelegate(), test_data)
features = feature_file_parser.Parse()
self.assertEqual(4, len(features))
self.assertEqual('SHORT', features[0].name)
self.assertEqual('"Short"', features[0].value)
self.assertEqual('TWO_LINE_FEATURE_A', features[1].name)
self.assertEqual('"TwoLineFeatureA"', features[1].value)
self.assertEqual('TWO_LINE_FEATURE_B', features[2].name)
self.assertEqual('"TwoLineFeatureB"', features[2].value)
self.assertEqual('FEATURE_WITH_A_VERY_LONG_NAME_THAT_WILL_HAVE_TO_WRAP',
features[3].name)
self.assertEqual('"FeatureWithAVeryLongNameThatWillHaveToWrap"',
features[3].value)
def testCppSyntax(self):
test_data = """
// Mismatched name
const base::Feature kMismatchedFeature{"MismatchedName",
base::FEATURE_DISABLED_BY_DEFAULT};
namespace myfeature {
// In a namespace
const base::Feature kSomeFeature{"SomeFeature",
base::FEATURE_DISABLED_BY_DEFAULT};
}
// Defined with equals sign
const base::Feature kFoo = {"Foo", base::FEATURE_DISABLED_BY_DEFAULT};
// Build config-specific base::Feature
#if defined(OS_ANDROID)
const base::Feature kAndroidOnlyFeature{"AndroidOnlyFeature",
base::FEATURE_DISABLED_BY_DEFAULT};
#endif
// Value depends on build config
const base::Feature kMaybeEnabled{"MaybeEnabled",
#if defined(OS_ANDROID)
base::FEATURE_DISABLED_BY_DEFAULT
#else
base::FEATURE_ENABLED_BY_DEFAULT
#endif
};
""".split('\n')
feature_file_parser = java_cpp_utils.CppConstantParser(
java_cpp_features.FeatureParserDelegate(), test_data)
features = feature_file_parser.Parse()
self.assertEqual(5, len(features))
self.assertEqual('MISMATCHED_FEATURE', features[0].name)
self.assertEqual('"MismatchedName"', features[0].value)
self.assertEqual('SOME_FEATURE', features[1].name)
self.assertEqual('"SomeFeature"', features[1].value)
self.assertEqual('FOO', features[2].name)
self.assertEqual('"Foo"', features[2].value)
self.assertEqual('ANDROID_ONLY_FEATURE', features[3].name)
self.assertEqual('"AndroidOnlyFeature"', features[3].value)
self.assertEqual('MAYBE_ENABLED', features[4].name)
self.assertEqual('"MaybeEnabled"', features[4].value)
def testNotYetSupported(self):
# Negative test for cases we don't yet support, to ensure we don't misparse
# these until we intentionally add proper support.
test_data = """
// Not currently supported: name depends on C++ directive
const base::Feature kNameDependsOnOs{
#if defined(OS_ANDROID)
"MaybeName1",
#else
"MaybeName2",
#endif
base::FEATURE_DISABLED_BY_DEFAULT};
// Not currently supported: feature named with a constant instead of literal
const base::Feature kNamedAfterConstant{kNamedStringConstant,
base::FEATURE_DISABLED_BY_DEFAULT};
""".split('\n')
feature_file_parser = java_cpp_utils.CppConstantParser(
java_cpp_features.FeatureParserDelegate(), test_data)
features = feature_file_parser.Parse()
self.assertEqual(0, len(features))
def testTreatWebViewLikeOneWord(self):
test_data = """
const base::Feature kSomeWebViewFeature{"SomeWebViewFeature",
base::FEATURE_DISABLED_BY_DEFAULT};
const base::Feature kWebViewOtherFeature{"WebViewOtherFeature",
base::FEATURE_ENABLED_BY_DEFAULT};
const base::Feature kFeatureWithPluralWebViews{
"FeatureWithPluralWebViews",
base::FEATURE_ENABLED_BY_DEFAULT};
""".split('\n')
feature_file_parser = java_cpp_utils.CppConstantParser(
java_cpp_features.FeatureParserDelegate(), test_data)
features = feature_file_parser.Parse()
self.assertEqual('SOME_WEBVIEW_FEATURE', features[0].name)
self.assertEqual('"SomeWebViewFeature"', features[0].value)
self.assertEqual('WEBVIEW_OTHER_FEATURE', features[1].name)
self.assertEqual('"WebViewOtherFeature"', features[1].value)
self.assertEqual('FEATURE_WITH_PLURAL_WEBVIEWS', features[2].name)
self.assertEqual('"FeatureWithPluralWebViews"', features[2].value)
def testSpecialCharacters(self):
test_data = r"""
const base::Feature kFeatureWithEscapes{"Weird\tfeature\"name\n",
base::FEATURE_DISABLED_BY_DEFAULT};
const base::Feature kFeatureWithEscapes2{
"Weird\tfeature\"name\n",
base::FEATURE_ENABLED_BY_DEFAULT};
""".split('\n')
feature_file_parser = java_cpp_utils.CppConstantParser(
java_cpp_features.FeatureParserDelegate(), test_data)
features = feature_file_parser.Parse()
self.assertEqual('FEATURE_WITH_ESCAPES', features[0].name)
self.assertEqual(r'"Weird\tfeature\"name\n"', features[0].value)
self.assertEqual('FEATURE_WITH_ESCAPES2', features[1].name)
self.assertEqual(r'"Weird\tfeature\"name\n"', features[1].value)
def testNoBaseNamespacePrefix(self):
test_data = """
const Feature kSomeFeature{"SomeFeature", FEATURE_DISABLED_BY_DEFAULT};
""".split('\n')
feature_file_parser = java_cpp_utils.CppConstantParser(
java_cpp_features.FeatureParserDelegate(), test_data)
features = feature_file_parser.Parse()
self.assertEqual('SOME_FEATURE', features[0].name)
self.assertEqual('"SomeFeature"', features[0].value)
if __name__ == '__main__':
unittest.main()

@ -14,130 +14,20 @@ from util import build_utils
from util import java_cpp_utils
class _String(object):
def __init__(self, name, value, comments):
self.name = java_cpp_utils.KCamelToShouty(name)
self.value = value
self.comments = '\n'.join(' ' + x for x in comments)
def Format(self):
return '%s\n public static final String %s = %s;' % (
self.comments, self.name, self.value)
def ParseTemplateFile(lines):
package_re = re.compile(r'^package (.*);')
class_re = re.compile(r'.*class (.*) {')
package = ''
class_name = ''
for line in lines:
package_line = package_re.match(line)
if package_line:
package = package_line.groups()[0]
class_line = class_re.match(line)
if class_line:
class_name = class_line.groups()[0]
break
return package, class_name
# TODO(crbug.com/937282): It should be possible to parse a file for more than
# string constants. However, this currently only handles extracting string
# constants from a file (and all string constants from that file). Work will
# be needed if we want to annotate specific constants or non string constants
# in the file to be parsed.
class StringFileParser(object):
SINGLE_LINE_COMMENT_RE = re.compile(r'\s*(// [^\n]*)')
class StringParserDelegate(java_cpp_utils.CppConstantParser.Delegate):
STRING_RE = re.compile(r'\s*const char k(.*)\[\]\s*=')
VALUE_RE = re.compile(r'\s*("(?:\"|[^"])*")\s*;')
def __init__(self, lines, path=''):
self._lines = lines
self._path = path
self._in_string = False
self._in_comment = False
self._package = ''
self._current_comments = []
self._current_name = ''
self._current_value = ''
self._strings = []
def _ExtractVariable(self, line):
match = StringFileParser.STRING_RE.match(line)
def ExtractConstantName(self, line):
match = StringParserDelegate.STRING_RE.match(line)
return match.group(1) if match else None
def _ExtractValue(self, line):
match = StringFileParser.VALUE_RE.search(line)
def ExtractValue(self, line):
match = StringParserDelegate.VALUE_RE.search(line)
return match.group(1) if match else None
def _Reset(self):
self._current_comments = []
self._current_name = ''
self._current_value = ''
self._in_string = False
self._in_comment = False
def _AppendString(self):
self._strings.append(
_String(self._current_name, self._current_value,
self._current_comments))
self._Reset()
def _ParseValue(self, line):
current_value = self._ExtractValue(line)
if current_value is not None:
self._current_value = current_value
self._AppendString()
else:
self._Reset()
def _ParseComment(self, line):
comment_line = StringFileParser.SINGLE_LINE_COMMENT_RE.match(line)
if comment_line:
self._current_comments.append(comment_line.groups()[0])
self._in_comment = True
self._in_string = True
return True
else:
self._in_comment = False
return False
def _ParseString(self, line):
current_name = self._ExtractVariable(line)
if current_name is not None:
self._current_name = current_name
current_value = self._ExtractValue(line)
if current_value is not None:
self._current_value = current_value
self._AppendString()
else:
self._in_string = True
return True
else:
self._in_string = False
return False
def _ParseLine(self, line):
if not self._in_string:
if not self._ParseString(line):
self._ParseComment(line)
return
if self._in_comment:
if self._ParseComment(line):
return
if not self._ParseString(line):
self._Reset()
return
if self._in_string:
self._ParseValue(line)
def Parse(self):
for line in self._lines:
self._ParseLine(line)
return self._strings
def CreateJavaConstant(self, name, value, comments):
return java_cpp_utils.JavaString(name, value, comments)
def _GenerateOutput(template, source_paths, template_path, strings):
@ -166,7 +56,9 @@ def _GenerateOutput(template, source_paths, template_path, strings):
def _ParseStringFile(path):
with open(path) as f:
return StringFileParser(f.readlines(), path).Parse()
string_file_parser = java_cpp_utils.CppConstantParser(
StringParserDelegate(), f.readlines())
return string_file_parser.Parse()
def _Generate(source_paths, template_path):
@ -174,10 +66,8 @@ def _Generate(source_paths, template_path):
lines = f.readlines()
template = ''.join(lines)
package, class_name = ParseTemplateFile(lines)
package_path = package.replace('.', os.path.sep)
file_name = class_name + '.java'
output_path = os.path.join(package_path, file_name)
package, class_name = java_cpp_utils.ParseTemplateFile(lines)
output_path = java_cpp_utils.GetJavaFilePath(package, class_name)
strings = []
for source_path in source_paths:
strings.extend(_ParseStringFile(source_path))
@ -189,17 +79,15 @@ def _Generate(source_paths, template_path):
def _Main(argv):
parser = argparse.ArgumentParser()
parser.add_argument(
'--srcjar',
required=True,
help='When specified, a .srcjar at the given path is '
'created instead of individual .java files.')
parser.add_argument('--srcjar',
required=True,
help='The path at which to generate the .srcjar file')
parser.add_argument(
'--template',
required=True,
help='Can be used to provide a context into which the'
'new string constants will be inserted.')
parser.add_argument('--template',
required=True,
help='The template file with which to generate the Java '
'class. Must have "{NATIVE_STRINGS}" somewhere in '
'the template.')
parser.add_argument(
'inputs', nargs='+', help='Input file(s)', metavar='INPUTFILE')

@ -12,6 +12,7 @@ This test suite contains various tests for the C++ -> Java string generator.
import unittest
import java_cpp_strings
from util import java_cpp_utils
class _TestStringsParser(unittest.TestCase):
@ -36,7 +37,9 @@ const char kAnotherSwitch[] = "another-value";
// Comment followed by nothing.
""".split('\n')
strings = java_cpp_strings.StringFileParser(test_data).Parse()
string_file_parser = java_cpp_utils.CppConstantParser(
java_cpp_strings.StringParserDelegate(), test_data)
strings = string_file_parser.Parse()
self.assertEqual(2, len(strings))
self.assertEqual('A_SWITCH', strings[0].name)
self.assertEqual('"a-value"', strings[0].value)
@ -82,7 +85,9 @@ const char kConcatenateMultipleStringLiterals[] =
"first line"
"second line";
""".split('\n')
strings = java_cpp_strings.StringFileParser(test_data).Parse()
string_file_parser = java_cpp_utils.CppConstantParser(
java_cpp_strings.StringParserDelegate(), test_data)
strings = string_file_parser.Parse()
self.assertEqual(9, len(strings))
self.assertEqual('A_STRING', strings[0].name)
self.assertEqual('"a-value"', strings[0].value)
@ -113,7 +118,9 @@ const char kSomeWebViewSwitch[] = "some-webview-switch";
const char kWebViewOtherSwitch[] = "webview-other-switch";
const char kSwitchWithPluralWebViews[] = "switch-with-plural-webviews";
""".split('\n')
strings = java_cpp_strings.StringFileParser(test_data).Parse()
string_file_parser = java_cpp_utils.CppConstantParser(
java_cpp_strings.StringParserDelegate(), test_data)
strings = string_file_parser.Parse()
self.assertEqual('SOME_WEBVIEW_SWITCH', strings[0].name)
self.assertEqual('"some-webview-switch"', strings[0].value)
self.assertEqual('WEBVIEW_OTHER_SWITCH', strings[1].name)
@ -123,16 +130,10 @@ const char kSwitchWithPluralWebViews[] = "switch-with-plural-webviews";
def testTemplateParsing(self):
test_data = """
// Copyright {YEAR} The Chromium Authors. All rights reserved.
// 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.
// This file is autogenerated by
// {SCRIPT_NAME}
// From
// {SOURCE_PATH}, and
// {TEMPLATE_PATH}
package my.java.package;
public any sort of class MyClass {{
@ -141,7 +142,7 @@ public any sort of class MyClass {{
}}
""".split('\n')
package, class_name = java_cpp_strings.ParseTemplateFile(test_data)
package, class_name = java_cpp_utils.ParseTemplateFile(test_data)
self.assertEqual('my.java.package', package)
self.assertEqual('MyClass', class_name)

@ -13,6 +13,12 @@ def GetScriptName():
return os.path.basename(os.path.abspath(sys.argv[0]))
def GetJavaFilePath(java_package, class_name):
package_path = java_package.replace('.', os.path.sep)
file_name = class_name + '.java'
return os.path.join(package_path, file_name)
def KCamelToShouty(s):
"""Convert |s| from kCamelCase or CamelCase to SHOUTY_CASE.
@ -32,3 +38,159 @@ def KCamelToShouty(s):
# Add _ between lower -> upper transitions.
s = re.sub(r'([^A-Z_0-9])([A-Z])', r'\1_\2', s)
return s.upper()
class JavaString(object):
def __init__(self, name, value, comments):
self.name = KCamelToShouty(name)
self.value = value
self.comments = '\n'.join(' ' + x for x in comments)
def Format(self):
return '%s\n public static final String %s = %s;' % (
self.comments, self.name, self.value)
def ParseTemplateFile(lines):
package_re = re.compile(r'^package (.*);')
class_re = re.compile(r'.*class (.*) {')
package = ''
class_name = ''
for line in lines:
package_line = package_re.match(line)
if package_line:
package = package_line.groups()[0]
class_line = class_re.match(line)
if class_line:
class_name = class_line.groups()[0]
break
return package, class_name
# TODO(crbug.com/937282): Work will be needed if we want to annotate specific
# constants in the file to be parsed.
class CppConstantParser(object):
"""Parses C++ constants, retaining their comments.
The Delegate subclass is responsible for matching and extracting the
constant's variable name and value, as well as generating an object to
represent the Java representation of this value.
"""
SINGLE_LINE_COMMENT_RE = re.compile(r'\s*(// [^\n]*)')
class Delegate(object):
def ExtractConstantName(self, line):
"""Extracts a constant's name from line or None if not a match."""
raise NotImplementedError()
def ExtractValue(self, line):
"""Extracts a constant's value from line or None if not a match."""
raise NotImplementedError()
def CreateJavaConstant(self, name, value, comments):
"""Creates an object representing the Java analog of a C++ constant.
CppConstantParser will not interact with the object created by this
method. Instead, it will store this value in a list and return a list of
all objects from the Parse() method. In this way, the caller may define
whatever class suits their need.
Args:
name: the constant's variable name, as extracted by
ExtractConstantName()
value: the constant's value, as extracted by ExtractValue()
comments: the code comments describing this constant
"""
raise NotImplementedError()
def __init__(self, delegate, lines):
self._delegate = delegate
self._lines = lines
self._in_variable = False
self._in_comment = False
self._package = ''
self._current_comments = []
self._current_name = ''
self._current_value = ''
self._constants = []
def _ExtractVariable(self, line):
match = StringFileParser.STRING_RE.match(line)
return match.group(1) if match else None
def _ExtractValue(self, line):
match = StringFileParser.VALUE_RE.search(line)
return match.group(1) if match else None
def _Reset(self):
self._current_comments = []
self._current_name = ''
self._current_value = ''
self._in_variable = False
self._in_comment = False
def _AppendConstant(self):
self._constants.append(
self._delegate.CreateJavaConstant(self._current_name,
self._current_value,
self._current_comments))
self._Reset()
def _ParseValue(self, line):
current_value = self._delegate.ExtractValue(line)
if current_value is not None:
self._current_value = current_value
self._AppendConstant()
else:
self._Reset()
def _ParseComment(self, line):
comment_line = CppConstantParser.SINGLE_LINE_COMMENT_RE.match(line)
if comment_line:
self._current_comments.append(comment_line.groups()[0])
self._in_comment = True
self._in_variable = True
return True
else:
self._in_comment = False
return False
def _ParseVariable(self, line):
current_name = self._delegate.ExtractConstantName(line)
if current_name is not None:
self._current_name = current_name
current_value = self._delegate.ExtractValue(line)
if current_value is not None:
self._current_value = current_value
self._AppendConstant()
else:
self._in_variable = True
return True
else:
self._in_variable = False
return False
def _ParseLine(self, line):
if not self._in_variable:
if not self._ParseVariable(line):
self._ParseComment(line)
return
if self._in_comment:
if self._ParseComment(line):
return
if not self._ParseVariable(line):
self._Reset()
return
if self._in_variable:
self._ParseValue(line)
def Parse(self):
"""Returns a list of objects representing C++ constants.
Each object in the list was created by Delegate.CreateJavaValue().
"""
for line in self._lines:
self._ParseLine(line)
return self._constants

@ -607,6 +607,81 @@ if (enable_java_templates) {
}
}
# Declare a target for generating Java classes with string constants matching
# those found in C++ base::Feature declarations, using a python script.
#
# This target will create a single .srcjar. Adding this target to an
# android_library target's srcjar_deps will make the generated java files be
# included in that library's final outputs.
#
# Variables
# sources: list of files to be processed by the script. For each
# base::Feature in the source files, the script will add a
# corresponding Java string for that feature's name to the
# specified template file.
# Example
# java_cpp_features("foo_features") {
# sources = [
# "src/foo_features.cc",
# ]
# template = "src/templates/FooFeatures.java.tmpl
# }
#
# foo_features.cc:
#
# // A feature.
# const base::Feature kSomeFeature{"SomeFeature",
# base::FEATURE_DISABLED_BY_DEFAULT};
#
# FooFeatures.java.tmpl
#
# // Copyright $YEAR 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 my.java.package;
#
# public final class FooFeatures {{
# // ...snip...
# {NATIVE_STRINGS}
# // ...snip...
# // Do not instantiate this class.
# private FooFeatures() {{}}
# }}
#
# result:
# A FooFeatures.java file, defining a class named FooFeatures in the package
# my.java.package.
template("java_cpp_features") {
set_sources_assignment_filter([])
action_with_pydeps(target_name) {
forward_variables_from(invoker,
[
"sources",
"testonly",
"visibility",
])
# The sources aren't compiled so don't check their dependencies.
check_includes = false
script = "//build/android/gyp/java_cpp_features.py"
_srcjar_path = "${target_gen_dir}/${target_name}.srcjar"
_rebased_srcjar_path = rebase_path(_srcjar_path, root_build_dir)
_rebased_sources = rebase_path(invoker.sources, root_build_dir)
_rebased_template = rebase_path(invoker.template, root_build_dir)
args = [
"--srcjar=$_rebased_srcjar_path",
"--template=$_rebased_template",
]
args += _rebased_sources
sources += [ invoker.template ]
outputs = [ _srcjar_path ]
}
}
# Declare a target for processing a Jinja template.
#
# Variables

@ -146,6 +146,10 @@ class
}
```
## See also
* [Accessing C++ Switches In Java](android_accessing_cpp_switches_in_java.md)
* [Accessing C++ Features In Java](android_accessing_cpp_features_in_java.md)
## Code
* [Generator
code](https://cs.chromium.org/chromium/src/build/android/gyp/java_cpp_enum.py?dr=C&sq=package:chromium)

@ -0,0 +1,172 @@
# Accessing C++ Features In Java
[TOC]
## Introduction
Accessing C++ `base::Features` in Java is implemented via a Python script which
analyzes the `*_features.cc` file and generates the corresponding Java class,
based on a template file. The template file must be specified in the GN target.
This outputs Java String constants which represent the name of the
`base::Feature`.
## Usage
1. Create a template file (ex. `FooFeatures.java.tmpl`). Change "Copyright
2020" to be whatever the year is at the time of writing (as you would for any
other file).
```java
// Copyright 2020 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.foo;
// Be sure to escape any curly braces in your template by doubling as
// follows.
/**
* Contains features that are specific to the foo project.
*/
public final class FooFeatures {{
{NATIVE_FEATURES}
// Prevents instantiation.
private FooFeatures() {{}}
}}
```
2. Add a new build target and add it to the `srcjar_deps` of an
`android_library` target:
```gn
if (is_android) {
import("//build/config/android/rules.gni")
}
if (is_android) {
java_cpp_features("java_features_srcjar") {
# External code should depend on ":foo_java" instead.
visibility = [ ":*" ]
sources = [
"//base/android/foo_features.cc",
]
template = "//base/android/java_templates/FooFeatures.java.tmpl"
}
# If there's already an android_library target, you can add
# java_features_srcjar to that target's srcjar_deps. Otherwise, the best
# practice is to create a new android_library just for this target.
android_library("foo_java") {
srcjar_deps = [ ":java_features_srcjar" ]
}
}
```
3. The generated file `out/Default/gen/.../org/chromium/foo/FooFeatures.java`
would contain:
```java
// Copyright $YEAR 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.foo;
// Be sure to escape any curly braces in your template by doubling as
// follows.
/**
* Contains features that are specific to the foo project.
*/
public final class FooFeatures {
// This following string constants were inserted by
// java_cpp_features.py
// From
// ../../base/android/foo_features.cc
// Into
// ../../base/android/java_templates/FooFeatures.java.tmpl
// Documentation for the C++ Feature is copied here.
public static final String SOME_FEATURE = "SomeFeature";
// ...snip...
// Prevents instantiation.
private FooFeatures() {}
}
```
### Troubleshooting
The script only supports limited syntaxes for declaring C++ base::Features. You
may see an error like the following during compilation:
```
...
org/chromium/foo/FooFeatures.java:41: error: duplicate declaration of field: MY_FEATURE
public static final String MY_FEATURE = "MyFeature";
```
This can happen if you've re-declared a feature for mutually-exclsuive build
configs (ex. the feature is enabled-by-default for one config, but
disabled-by-default for another). Example:
```c++
#if defined(...)
const base::Feature kMyFeature{
"MyFeature", base::FEATURE_ENABLED_BY_DEFAULT};
#else
const base::Feature kMyFeature{
"MyFeature", base::FEATURE_DISABLED_BY_DEFAULT};
#endif
```
The `java_cpp_features` rule doesn't know how to evaluate C++ preprocessor
directives, so it generates two identical Java fields (which is what the
compilation error is complaining about). Fortunately, the workaround is fairly
simple. Rewrite the definition to only use directives around the enabled state:
```c++
const base::Feature kMyFeature{
"MyFeature",
#if defined(...)
base::FEATURE_ENABLED_BY_DEFAULT
#else
base::FEATURE_DISABLED_BY_DEFAULT
#endif
};
```
## Checking if a Feature is enabled
The standard pattern is to create a `FooFeatureList.java` class with an
`isEnabled()` method (ex.
[`ContentFeatureList`](/content/public/android/java/src/org/chromium/content_public/browser/ContentFeatureList.java)).
This should call into C++ (ex.
[`content_feature_list`](/content/browser/android/content_feature_list.cc)),
where a subset of features are exposed via the `kFeaturesExposedToJava` array.
You can either add your `base::Feature` to an existing `feature_list` or create
a new `FeatureList` class if no existing one is suitable. Then you can check the
enabled state like so:
```java
// It's OK if ContentFeatureList checks FooFeatures.*, so long as
// content_feature_list.cc exposes `kMyFeature`.
if (ContentFeatureList.isEnabled(FooFeatures.MY_FEATURE)) {
// ...
}
```
At the moment, `base::Features` must be explicitly exposed to Java this way, in
whichever layer needs to access their state. See https://crbug.com/1060097.
## See also
* [Accessing C++ Enums In Java](android_accessing_cpp_enums_in_java.md)
* [Accessing C++ Switches In Java](android_accessing_cpp_switches_in_java.md)
## Code
* [Generator code](/build/android/gyp/java_cpp_features.py) and
[Tests](/build/android/gyp/java_cpp_features_tests.py)
* [GN template](/build/config/android/rules.gni)

@ -10,9 +10,11 @@ template file. The template file must be specified in the GN target.
## Usage
1. Create a template file (ex. `FooSwitches.java.tmpl`)
1. Create a template file (ex. `FooSwitches.java.tmpl`). Change "Copyright
2020" to be whatever the year is at the time of writing (as you would for any
other file).
```java
// Copyright $YEAR The Chromium Authors. All rights reserved.
// Copyright 2020 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.
@ -93,6 +95,10 @@ template file. The template file must be specified in the GN target.
}
```
## See also
* [Accessing C++ Enums In Java](android_accessing_cpp_enums_in_java.md)
* [Accessing C++ Features In Java](android_accessing_cpp_features_in_java.md)
## Code
* [Generator
code](https://cs.chromium.org/chromium/src/build/android/gyp/java_cpp_strings.py?dr=C&sq=package:chromium)