blob: 9bccabbbae6fc212f60f1f169d4f472fa6e6373f [file] [log] [blame]
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright 2015 The Chromium OS Authors. All rights reserved.
# 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
from datetime import date
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 proto3')
class Message(object):
"""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, proto3):
"""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.
proto3: The field is proto3 style or not.
"""
self.fields.append(
Field(
repeated=attribute == 'repeated',
type_=field_type,
name=field_name,
proto3=proto3))
class Enum(object):
"""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(object):
"""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('//'):
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), False)
elif current_message_stack and field_match3:
current_message_stack[-1].AddField(
field_match3.group(1).strip(), field_match3.group(2),
field_match3.group(3), True)
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
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 Chromium OS 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 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': 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 Chromium OS 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 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': 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 + "}\\n";
return output;
}
"""
singular_field = """
if (value.has_%(name)s()) {
output += indent + " %(name)s: ";
base::StringAppendF(&output, %(format)s);
output += "\\n";
}"""
proto3_singular_field = """
output += indent + " %(name)s: ";
base::StringAppendF(&output, %(format)s);
output += "\\n";
"""
repeated_field = """
output += indent + " %(name)s: {";
for (int i = 0; i < value.%(name)s_size(); ++i) {
if (i > 0) {
base::StringAppendF(&output, ", ");
}
base::StringAppendF(&output, %(format)s);
}
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 + 2).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
elif field.proto3:
value_get = singular_field_get % {'name': field.name}
field_code = proto3_singular_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}
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) 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') as header_file:
with open(impl_file_path, 'w') 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()