blob: 7290b72c484718f7036e001d34afb924660dc86b [file] [log] [blame] [edit]
#!/usr/bin/env python3
# Copyright 2015 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""A C++ code generator for printing protobufs which use the LITE_RUNTIME.
Normally printing a protobuf would be done with Message::DebugString(). However,
this is not available when using only MessageLite. This script generates code to
emulate Message::DebugString() without using reflection. The input must be a
valid .proto file.
Usage: proto_print.py [--package-dir=pac] [--subdir=foo] <bar.proto>
Files named print_bar_proto.h and print_bar_proto.cc will be created in the
current working directory.
"""
from __future__ import print_function
import argparse
import collections
import datetime
import os
import re
import subprocess
import sys
# Holds information about a protobuf message field.
#
# Attributes:
# repeated: Whether the field is a repeated field.
# type_: The type of the field. E.g. int32.
# name: The name of the field.
Field = collections.namedtuple("Field", "repeated type_ name")
class Message:
"""Holds information about a protobuf message.
Attributes:
name: The name of the message.
fields: A list of Field tuples.
"""
def __init__(self, name):
"""Initializes a Message instance.
Args:
name: The protobuf message name.
"""
self.name = name
self.fields = []
def AddField(self, attribute, field_type, field_name):
"""Adds a new field to the message.
Args:
attribute: This should be 'optional', 'required', or 'repeated'.
field_type: The type of the field. E.g. int32.
field_name: The name of the field.
"""
self.fields.append(
Field(
repeated=attribute == "repeated",
type_=field_type,
name=field_name,
)
)
class Enum:
"""Holds information about a protobuf enum.
Attributes:
name: The name of the enum.
values: A list of enum value names.
"""
def __init__(self, name):
"""Initializes a Enum instance.
Args:
name: The protobuf enum name.
"""
self.name = name
self.values = []
def AddValue(self, value_name):
"""Adds a new value to the enum.
Args:
value_name: The name of the value.
"""
self.values.append(value_name)
class Oneof:
"""Holds information about a protobuf Oneof.
Attributes:
name: The name of the OnoOf.
"""
def __init__(self, name):
"""Initializes a OnoOf instance.
Args:
name: The protobuf OnoOf name.
"""
self.name = name
def ParseProto(input_file):
"""Parses a proto file and returns a tuple of parsed information.
Args:
input_file: The proto file to parse.
Returns:
A tuple in the form (package, imports, messages, enums) where
package: A string holding the proto package.
imports: A list of strings holding proto imports.
messages: A list of Message objects; one for each message in the proto.
enums: A list of Enum objects; one for each enum in the proto.
"""
package = ""
imports = []
messages = []
enums = []
current_message_stack = []
current_enum = None
current_oneof = None
package_re = re.compile(r"package\s+([.\w]+);")
import_re = re.compile(r'import\s+"([\w]*/)*(\w+).proto";')
message_re = re.compile(r"message\s+(\w+)\s*{")
field_re = re.compile(r"(optional|required|repeated)\s+(\w+)\s+(\w+)\s*=")
field_re3 = re.compile(r"(|repeated\s+)([.\w]+)\s+(\w+)\s*=")
enum_re = re.compile(r"enum\s+(\w+)\s*{")
enum_value_re = re.compile(r"(\w+)\s*=")
oneof_re = re.compile(r"oneof\s+(\w+)\s*{")
for line in input_file:
line = line.strip()
if not line or line.startswith("//") or line.startswith("reserved"):
continue
msg_match = message_re.search(line)
enum_match = enum_re.search(line)
oneof_match = oneof_re.search(line)
field_match = field_re.search(line)
field_match3 = field_re3.search(line)
package_match = package_re.search(line)
import_match = import_re.search(line)
# Look for a message definition.
if msg_match:
prefix = ""
if current_message_stack:
prefix = (
"::".join([m.name for m in current_message_stack]) + "::"
)
current_message_stack.append(Message(prefix + msg_match.group(1)))
elif current_message_stack and oneof_match:
if current_oneof:
raise Exception("Unsupported nested oneof")
# TODO(b/220231404): Support the printing of oneof field.
current_oneof = Oneof(prefix + oneof_match.group(1))
# Look for a message field definition.
elif current_message_stack and field_match:
current_message_stack[-1].AddField(
field_match.group(1),
field_match.group(2),
field_match.group(3),
)
elif (
current_message_stack
and field_match3
and field_match3.group(2) != "option"
):
current_message_stack[-1].AddField(
field_match3.group(1).strip(),
field_match3.group(2),
field_match3.group(3),
)
elif enum_match:
prefix = ""
if current_message_stack:
prefix = "_".join([m.name for m in current_message_stack]) + "_"
current_enum = Enum(prefix + enum_match.group(1))
continue
# Look for an enum value.
elif current_enum:
match = enum_value_re.search(line)
if match:
prefix = ""
if current_message_stack:
prefix = current_enum.name + "_"
current_enum.AddValue(prefix + match.group(1))
# Look for a package statement.
elif package_match:
package = package_match.group(1)
# Look for an import statement.
elif import_match:
imports.append(import_match.group(2))
# Close off the current scope. Enums first because they can't be nested.
if line[-1] == "}":
if current_enum:
enums.append(current_enum)
current_enum = None
elif current_oneof:
# TODO(b/220231404): Support the printing of oneof field.
current_oneof = None
elif current_message_stack:
messages.append(current_message_stack.pop())
else:
raise Exception("Closing unknown scope")
# Make sure we parse the proto file correctly.
if current_enum:
raise Exception(f'The current_enum "{current_enum.name}" is not empty')
if current_oneof:
raise Exception(
f'The current_oneof "{current_oneof.name}" is not empty'
)
if current_message_stack:
name_stack = [msg.name for msg in current_message_stack]
raise Exception(f"The current_message_stack {name_stack} is not empty")
return package, imports, messages, enums
def GenerateFileHeaders(
proto_name,
package,
imports,
subdir,
proto_include_override,
header_file_name,
header_file,
impl_file,
package_dir,
):
"""Generates and prints file headers.
Args:
proto_name: The name of the proto file.
package: The protobuf package.
imports: A list of imported protos.
subdir: The --subdir arg.
proto_include_override: Include directory override for the #include
statement in generated code
header_file_name: The header file name.
header_file: The header file handle, open for writing.
impl_file: The implementation file handle, open for writing.
package_dir: The package directory.
"""
if subdir:
guard_name = "%s_%s_PRINT_%s_PROTO_H_" % (
package_dir.upper(),
subdir.upper(),
proto_name.upper(),
)
package_with_subdir = "%s/%s" % (package_dir, subdir)
else:
guard_name = "%s_PRINT_%s_PROTO_H_" % (
package_dir.upper(),
proto_name.upper(),
)
package_with_subdir = package_dir
guard_name = guard_name.replace("-", "_")
proto_include_dir = package_with_subdir
if proto_include_override is not None:
proto_include_dir = proto_include_override
namespace = package.replace(".", "::")
includes = "\n".join(
[
'#include "%(package_with_subdir)s/print_%(import)s_proto.h"'
% {
"package_with_subdir": package_with_subdir,
"import": current_import,
}
for current_import in imports
]
)
header = """\
// Copyright %(year)s The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// THIS CODE IS GENERATED.
// Generated with command:
// %(cmd)s
#ifndef %(guard_name)s
#define %(guard_name)s
#include <string>
#include <brillo/brillo_export.h>
#include "%(proto_include_dir)s/%(proto)s.pb.h"
namespace %(namespace)s {
""" % {
"year": datetime.date.today().year,
"guard_name": guard_name,
"namespace": namespace,
"proto": proto_name,
"proto_include_dir": proto_include_dir,
"cmd": " ".join(sys.argv),
}
impl = """\
// Copyright %(year)s The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// THIS CODE IS GENERATED.
// Generated with command:
// %(cmd)s
#include "%(package_with_subdir)s/%(header_file_name)s"
#include <inttypes.h>
#include <string>
#include <base/strings/string_number_conversions.h>
#include <base/strings/stringprintf.h>
%(includes)s
namespace %(namespace)s {
""" % {
"year": datetime.date.today().year,
"namespace": namespace,
"package_with_subdir": package_with_subdir,
"header_file_name": header_file_name,
"includes": includes,
"cmd": " ".join(sys.argv),
}
header_file.write(header)
impl_file.write(impl)
def GenerateFileFooters(
proto_name, package, subdir, header_file, impl_file, package_dir
):
"""Generates and prints file footers.
Args:
proto_name: The name of the proto file.
package: The protobuf package.
subdir: The --subdir arg.
header_file: The header file handle, open for writing.
impl_file: The implementation file handle, open for writing.
package_dir: The package directory.
"""
namespace = package.replace(".", "::")
if subdir:
guard_name = "%s_%s_PRINT_%s_PROTO_H_" % (
package_dir.upper(),
subdir.upper(),
proto_name.upper(),
)
else:
guard_name = "%s_PRINT_%s_PROTO_H_" % (
package_dir.upper(),
proto_name.upper(),
)
header = """
} // namespace %(namespace)s
#endif // %(guard_name)s
""" % {
"guard_name": guard_name,
"namespace": namespace,
}
impl = """
} // namespace %(namespace)s
""" % {
"namespace": namespace
}
header_file.write(header)
impl_file.write(impl)
def GenerateEnumPrinter(enum, header_file, impl_file):
"""Generates and prints a function to print an enum value.
Args:
enum: An Enum instance.
header_file: The header file handle, open for writing.
impl_file: The implementation file handle, open for writing.
"""
declare = """
std::string GetProtoDebugStringWithIndent(%(name)s value, int indent_size);
BRILLO_EXPORT std::string GetProtoDebugString(%(name)s value);""" % {
"name": enum.name
}
define_begin = """
std::string GetProtoDebugString(%(name)s value) {
return GetProtoDebugStringWithIndent(value, 0);
}
std::string GetProtoDebugStringWithIndent(%(name)s value, int indent_size) {
""" % {
"name": enum.name
}
define_end = """
return "<unknown>";
}
"""
condition = """
if (value == %(value_name)s) {
return "%(value_name)s";
}"""
header_file.write(declare)
impl_file.write(define_begin)
for value_name in enum.values:
impl_file.write(condition % {"value_name": value_name})
impl_file.write(define_end)
def GenerateMessagePrinter(message, header_file, impl_file):
"""Generates and prints a function to print a message.
Args:
message: A Message instance.
header_file: The header file handle, open for writing.
impl_file: The implementation file handle, open for writing.
"""
declare = """
std::string GetProtoDebugStringWithIndent(const %(name)s& value,
int indent_size);
BRILLO_EXPORT std::string GetProtoDebugString(const %(name)s& value);""" % {
"name": message.name
}
define_begin = """
std::string GetProtoDebugString(const %(name)s& value) {
return GetProtoDebugStringWithIndent(value, 0);
}
std::string GetProtoDebugStringWithIndent(const %(name)s& value,
int indent_size) {
std::string indent(indent_size, ' ');
std::string output = base::StringPrintf("[%%s] {\\n",
value.GetTypeName().c_str());
""" % {
"name": message.name
}
define_end = """
output += indent + "}";
return output;
}
"""
# As long as the field has a has_name method, call it to check whether the
# field exists. We don't need to print it if it doesn't.
singular_field = """
[]<typename T>(const T& value, int indent_size,
const std::string& indent, std::string& output) {
if constexpr (requires(T t) { t.has_%(name)s(); }) {
if (!value.has_%(name)s()) {
return;
}
}
output += indent + " %(name)s: ";
base::StringAppendF(&output, %(format)s);
output += "\\n";
}(value, indent_size, indent, output);"""
repeated_field = """
output += indent + " %(name)s: {";
for (int i = 0; i < value.%(name)s_size(); ++i) {
if (i > 0) {
output += ",";
}
output += "\\n " + indent;
base::StringAppendF(&output, %(format)s);
if (i == value.%(name)s_size() - 1) {
output += "\\n " + indent;
}
}
output += "}\\n";"""
singular_field_get = "value.%(name)s()"
repeated_field_get = "value.%(name)s(i)"
formats = {
"bool": '"%%s", %(value)s ? "true" : "false"',
"int32": '"%%" PRId32, %(value)s',
"int64": '"%%" PRId64, %(value)s',
"uint32": '"%%" PRIu32 " (0x%%08" PRIX32 ")", %(value)s, %(value)s',
"uint64": '"%%" PRIu64 " (0x%%016" PRIX64 ")", %(value)s, %(value)s',
"string": '"%%s", %(value)s.c_str()',
"bytes": """"%%s", base::HexEncode(%(value)s.data(),
%(value)s.size()).c_str()""",
}
subtype_format = (
'"%%s", GetProtoDebugStringWithIndent(%(value)s, '
"indent_size + %(indent_incr)s).c_str()"
)
header_file.write(declare)
impl_file.write(define_begin)
for field in message.fields:
if field.repeated:
value_get = repeated_field_get % {"name": field.name}
field_code = repeated_field
else:
value_get = singular_field_get % {"name": field.name}
field_code = singular_field
if field.type_ in formats:
value_format = formats[field.type_] % {"value": value_get}
else:
value_format = subtype_format % {
"value": value_get,
"indent_incr": 4 if field.repeated else 2,
}
impl_file.write(
field_code % {"name": field.name, "format": value_format}
)
impl_file.write(define_end)
def FormatFile(filename):
subprocess.call(["clang-format", "-i", "-style=Chromium", filename])
def main():
parser = argparse.ArgumentParser(description="print proto code generator")
parser.add_argument(
"--package-dir",
default="",
help=("Override for the package directory name"),
)
parser.add_argument(
"--subdir",
default="",
help=(
"The subdirectory under which the generated "
+ "file will reside in"
),
)
parser.add_argument(
"--proto-include-override",
default=None,
help=(
"Override for the include path of *.pb.h in" + "generated header"
),
)
parser.add_argument(
"--output-dir", default=".", help=("The output directory")
)
parser.add_argument("input_files", nargs="*")
args = parser.parse_args()
os.makedirs(args.output_dir, exist_ok=True)
for input_path in args.input_files:
with open(input_path, encoding="utf-8") as input_file:
package, imports, messages, enums = ParseProto(input_file)
package_dir = package
if args.package_dir != "":
package_dir = args.package_dir
proto_name = os.path.basename(input_path).rsplit(".", 1)[0]
header_file_name = "print_%s_proto.h" % proto_name
impl_file_name = "print_%s_proto.cc" % proto_name
header_file_path = os.path.join(args.output_dir, header_file_name)
impl_file_path = os.path.join(args.output_dir, impl_file_name)
with open(header_file_path, "w", encoding="utf-8") as header_file:
with open(impl_file_path, "w", encoding="utf-8") as impl_file:
GenerateFileHeaders(
proto_name,
package,
imports,
args.subdir,
args.proto_include_override,
header_file_name,
header_file,
impl_file,
package_dir,
)
for enum in enums:
GenerateEnumPrinter(enum, header_file, impl_file)
for message in messages:
GenerateMessagePrinter(message, header_file, impl_file)
GenerateFileFooters(
proto_name,
package,
args.subdir,
header_file,
impl_file,
package_dir,
)
FormatFile(header_file_path)
FormatFile(impl_file_path)
if __name__ == "__main__":
main()