417 lines
13 KiB
Python
Executable file
417 lines
13 KiB
Python
Executable file
#!/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()
|