#!/usr/bin/python2
#
# Copyright (C) 2015 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""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 [--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
# 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(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):
"""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(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)
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
package_re = re.compile(r'package\s+(\w+);')
import_re = re.compile(r'import\s+"(\w+).proto";')
message_re = re.compile(r'message\s+(\w+)\s*{')
field_re = re.compile(r'(optional|required|repeated)\s+(\w+)\s+(\w+)\s*=')
enum_re = re.compile(r'enum\s+(\w+)\s*{')
enum_value_re = re.compile(r'(\w+)\s*=')
for line in input_file:
line = line.strip()
if not line or line.startswith('//'):
continue
# Close off the current scope. Enums first because they can't be nested.
if line == '}':
if current_enum:
enums.append(current_enum)
current_enum = None
if current_message_stack:
messages.append(current_message_stack.pop())
continue
# Look for a message definition.
match = message_re.search(line)
if match:
prefix = ''
if current_message_stack:
prefix = '::'.join([m.name for m in current_message_stack]) + '::'
current_message_stack.append(Message(prefix + match.group(1)))
continue
# Look for a message field definition.
if current_message_stack:
match = field_re.search(line)
if match:
current_message_stack[-1].AddField(match.group(1),
match.group(2),
match.group(3))
continue
# Look for an enum definition.
match = enum_re.search(line)
if match:
current_enum = Enum(match.group(1))
continue
# Look for an enum value.
if current_enum:
match = enum_value_re.search(line)
if match:
current_enum.AddValue(match.group(1))
continue
# Look for a package statement.
match = package_re.search(line)
if match:
package = match.group(1)
# Look for an import statement.
match = import_re.search(line)
if match:
imports.append(match.group(1))
return package, imports, messages, enums
def GenerateFileHeaders(proto_name, package, imports, subdir, header_file_name,
header_file, impl_file):
"""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.
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.
"""
if subdir:
guard_name = '%s_%s_PRINT_%s_PROTO_H_' % (package.upper(),
subdir.upper(),
proto_name.upper())
package_with_subdir = '%s/%s' % (package, subdir)
else:
guard_name = '%s_PRINT_%s_PROTO_H_' % (package.upper(), proto_name.upper())
package_with_subdir = package
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.
#ifndef %(guard_name)s
#define %(guard_name)s
#include <string>
#include "%(package_with_subdir)s/%(proto)s.pb.h"
namespace %(package)s {
""" % {'year': date.today().year,
'guard_name': guard_name,
'package': package,
'proto': proto_name,
'package_with_subdir': package_with_subdir}
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.
#include "%(package_with_subdir)s/%(header_file_name)s"
#include <string>
#include <base/strings/string_number_conversions.h>
#include <base/strings/stringprintf.h>
%(includes)s
namespace %(package)s {
""" % {'year': date.today().year,
'package': package,
'package_with_subdir': package_with_subdir,
'header_file_name': header_file_name,
'includes': includes}
header_file.write(header)
impl_file.write(impl)
def GenerateFileFooters(proto_name, package, subdir, header_file, impl_file):
"""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.
"""
if subdir:
guard_name = '%s_%s_PRINT_%s_PROTO_H_' % (package.upper(),
subdir.upper(),
proto_name.upper())
else:
guard_name = '%s_PRINT_%s_PROTO_H_' % (package.upper(), proto_name.upper())
header = """
} // namespace %(package)s
#endif // %(guard_name)s
""" % {'guard_name': guard_name, 'package': package}
impl = """
} // namespace %(package)s
""" % {'package': package}
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);
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);
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";
}"""
repeated_field = """
output += indent + " %(name)s: {";
for (int i = 0; i < value.%(name)s_size(); ++i) {
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': '"%%d", %(value)s',
'int64': '"%%ld", %(value)s',
'uint32': '"%%u", %(value)s',
'uint64': '"%%lu", %(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
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('input_file')
parser.add_argument('--subdir', default='')
args = parser.parse_args()
with open(args.input_file) as input_file:
package, imports, messages, enums = ParseProto(input_file)
proto_name = os.path.basename(args.input_file).rsplit('.', 1)[0]
header_file_name = 'print_%s_proto.h' % proto_name
impl_file_name = 'print_%s_proto.cc' % proto_name
with open(header_file_name, 'w') as header_file:
with open(impl_file_name, 'w') as impl_file:
GenerateFileHeaders(proto_name, package, imports, args.subdir,
header_file_name, header_file, impl_file)
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)
FormatFile(header_file_name)
FormatFile(impl_file_name)
if __name__ == '__main__':
main()