1#!/usr/bin/python2 2 3# 4# Copyright (C) 2015 The Android Open Source Project 5# 6# Licensed under the Apache License, Version 2.0 (the "License"); 7# you may not use this file except in compliance with the License. 8# You may obtain a copy of the License at 9# 10# http://www.apache.org/licenses/LICENSE-2.0 11# 12# Unless required by applicable law or agreed to in writing, software 13# distributed under the License is distributed on an "AS IS" BASIS, 14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15# See the License for the specific language governing permissions and 16# limitations under the License. 17# 18 19"""A C++ code generator for printing protobufs which use the LITE_RUNTIME. 20 21Normally printing a protobuf would be done with Message::DebugString(). However, 22this is not available when using only MessageLite. This script generates code to 23emulate Message::DebugString() without using reflection. The input must be a 24valid .proto file. 25 26Usage: proto_print.py [--subdir=foo] <bar.proto> 27 28Files named print_bar_proto.h and print_bar_proto.cc will be created in the 29current working directory. 30""" 31 32from __future__ import print_function 33 34import argparse 35import collections 36from datetime import date 37import os 38import re 39import subprocess 40 41 42# Holds information about a protobuf message field. 43# 44# Attributes: 45# repeated: Whether the field is a repeated field. 46# type_: The type of the field. E.g. int32. 47# name: The name of the field. 48Field = collections.namedtuple('Field', 'repeated type_ name') 49 50 51class Message(object): 52 """Holds information about a protobuf message. 53 54 Attributes: 55 name: The name of the message. 56 fields: A list of Field tuples. 57 """ 58 59 def __init__(self, name): 60 """Initializes a Message instance. 61 62 Args: 63 name: The protobuf message name. 64 """ 65 self.name = name 66 self.fields = [] 67 68 def AddField(self, attribute, field_type, field_name): 69 """Adds a new field to the message. 70 71 Args: 72 attribute: This should be 'optional', 'required', or 'repeated'. 73 field_type: The type of the field. E.g. int32. 74 field_name: The name of the field. 75 """ 76 self.fields.append(Field(repeated=attribute == 'repeated', 77 type_=field_type, name=field_name)) 78 79 80class Enum(object): 81 """Holds information about a protobuf enum. 82 83 Attributes: 84 name: The name of the enum. 85 values: A list of enum value names. 86 """ 87 88 def __init__(self, name): 89 """Initializes a Enum instance. 90 91 Args: 92 name: The protobuf enum name. 93 """ 94 self.name = name 95 self.values = [] 96 97 def AddValue(self, value_name): 98 """Adds a new value to the enum. 99 100 Args: 101 value_name: The name of the value. 102 """ 103 self.values.append(value_name) 104 105 106def ParseProto(input_file): 107 """Parses a proto file and returns a tuple of parsed information. 108 109 Args: 110 input_file: The proto file to parse. 111 112 Returns: 113 A tuple in the form (package, imports, messages, enums) where 114 package: A string holding the proto package. 115 imports: A list of strings holding proto imports. 116 messages: A list of Message objects; one for each message in the proto. 117 enums: A list of Enum objects; one for each enum in the proto. 118 """ 119 package = '' 120 imports = [] 121 messages = [] 122 enums = [] 123 current_message_stack = [] 124 current_enum = None 125 package_re = re.compile(r'package\s+(\w+);') 126 import_re = re.compile(r'import\s+"([\w]*/)*(\w+).proto";') 127 message_re = re.compile(r'message\s+(\w+)\s*{') 128 field_re = re.compile(r'(optional|required|repeated)\s+(\w+)\s+(\w+)\s*=') 129 enum_re = re.compile(r'enum\s+(\w+)\s*{') 130 enum_value_re = re.compile(r'(\w+)\s*=') 131 for line in input_file: 132 line = line.strip() 133 if not line or line.startswith('//'): 134 continue 135 # Close off the current scope. Enums first because they can't be nested. 136 if line == '}': 137 if current_enum: 138 enums.append(current_enum) 139 current_enum = None 140 if current_message_stack: 141 messages.append(current_message_stack.pop()) 142 continue 143 # Look for a message definition. 144 match = message_re.search(line) 145 if match: 146 prefix = '' 147 if current_message_stack: 148 prefix = '::'.join([m.name for m in current_message_stack]) + '::' 149 current_message_stack.append(Message(prefix + match.group(1))) 150 continue 151 # Look for a message field definition. 152 if current_message_stack: 153 match = field_re.search(line) 154 if match: 155 current_message_stack[-1].AddField(match.group(1), 156 match.group(2), 157 match.group(3)) 158 continue 159 # Look for an enum definition. 160 match = enum_re.search(line) 161 if match: 162 current_enum = Enum(match.group(1)) 163 continue 164 # Look for an enum value. 165 if current_enum: 166 match = enum_value_re.search(line) 167 if match: 168 current_enum.AddValue(match.group(1)) 169 continue 170 # Look for a package statement. 171 match = package_re.search(line) 172 if match: 173 package = match.group(1) 174 # Look for an import statement. 175 match = import_re.search(line) 176 if match: 177 imports.append(match.group(2)) 178 return package, imports, messages, enums 179 180 181def GenerateFileHeaders(proto_name, package, imports, subdir, header_file_name, 182 header_file, impl_file): 183 """Generates and prints file headers. 184 185 Args: 186 proto_name: The name of the proto file. 187 package: The protobuf package. 188 imports: A list of imported protos. 189 subdir: The --subdir arg. 190 header_file_name: The header file name. 191 header_file: The header file handle, open for writing. 192 impl_file: The implementation file handle, open for writing. 193 """ 194 if subdir: 195 guard_name = '%s_%s_PRINT_%s_PROTO_H_' % (package.upper(), 196 subdir.upper(), 197 proto_name.upper()) 198 package_with_subdir = '%s/%s' % (package, subdir) 199 else: 200 guard_name = '%s_PRINT_%s_PROTO_H_' % (package.upper(), proto_name.upper()) 201 package_with_subdir = package 202 includes = '\n'.join( 203 ['#include "%(package_with_subdir)s/print_%(import)s_proto.h"' % { 204 'package_with_subdir': package_with_subdir, 205 'import': current_import} for current_import in imports]) 206 header = """\ 207// Copyright %(year)s The Chromium OS Authors. All rights reserved. 208// Use of this source code is governed by a BSD-style license that can be 209// found in the LICENSE file. 210 211// THIS CODE IS GENERATED. 212 213#ifndef %(guard_name)s 214#define %(guard_name)s 215 216#include <string> 217 218#include "%(package_with_subdir)s/%(proto)s.pb.h" 219 220namespace %(package)s { 221""" % {'year': date.today().year, 222 'guard_name': guard_name, 223 'package': package, 224 'proto': proto_name, 225 'package_with_subdir': package_with_subdir} 226 impl = """\ 227// Copyright %(year)s The Chromium OS Authors. All rights reserved. 228// Use of this source code is governed by a BSD-style license that can be 229// found in the LICENSE file. 230 231// THIS CODE IS GENERATED. 232 233#include "%(package_with_subdir)s/%(header_file_name)s" 234 235#include <string> 236 237#include <base/strings/string_number_conversions.h> 238#include <base/strings/stringprintf.h> 239 240%(includes)s 241 242namespace %(package)s { 243""" % {'year': date.today().year, 244 'package': package, 245 'package_with_subdir': package_with_subdir, 246 'header_file_name': header_file_name, 247 'includes': includes} 248 249 header_file.write(header) 250 impl_file.write(impl) 251 252 253def GenerateFileFooters(proto_name, package, subdir, header_file, impl_file): 254 """Generates and prints file footers. 255 256 Args: 257 proto_name: The name of the proto file. 258 package: The protobuf package. 259 subdir: The --subdir arg. 260 header_file: The header file handle, open for writing. 261 impl_file: The implementation file handle, open for writing. 262 """ 263 if subdir: 264 guard_name = '%s_%s_PRINT_%s_PROTO_H_' % (package.upper(), 265 subdir.upper(), 266 proto_name.upper()) 267 else: 268 guard_name = '%s_PRINT_%s_PROTO_H_' % (package.upper(), proto_name.upper()) 269 header = """ 270 271} // namespace %(package)s 272 273#endif // %(guard_name)s 274""" % {'guard_name': guard_name, 'package': package} 275 impl = """ 276} // namespace %(package)s 277""" % {'package': package} 278 279 header_file.write(header) 280 impl_file.write(impl) 281 282 283def GenerateEnumPrinter(enum, header_file, impl_file): 284 """Generates and prints a function to print an enum value. 285 286 Args: 287 enum: An Enum instance. 288 header_file: The header file handle, open for writing. 289 impl_file: The implementation file handle, open for writing. 290 """ 291 declare = """ 292std::string GetProtoDebugStringWithIndent(%(name)s value, int indent_size); 293std::string GetProtoDebugString(%(name)s value);""" % {'name': enum.name} 294 define_begin = """ 295std::string GetProtoDebugString(%(name)s value) { 296 return GetProtoDebugStringWithIndent(value, 0); 297} 298 299std::string GetProtoDebugStringWithIndent(%(name)s value, int indent_size) { 300""" % {'name': enum.name} 301 define_end = """ 302 return "<unknown>"; 303} 304""" 305 condition = """ 306 if (value == %(value_name)s) { 307 return "%(value_name)s"; 308 }""" 309 310 header_file.write(declare) 311 impl_file.write(define_begin) 312 for value_name in enum.values: 313 impl_file.write(condition % {'value_name': value_name}) 314 impl_file.write(define_end) 315 316 317def GenerateMessagePrinter(message, header_file, impl_file): 318 """Generates and prints a function to print a message. 319 320 Args: 321 message: A Message instance. 322 header_file: The header file handle, open for writing. 323 impl_file: The implementation file handle, open for writing. 324 """ 325 declare = """ 326std::string GetProtoDebugStringWithIndent(const %(name)s& value, 327 int indent_size); 328std::string GetProtoDebugString(const %(name)s& value);""" % {'name': 329 message.name} 330 define_begin = """ 331std::string GetProtoDebugString(const %(name)s& value) { 332 return GetProtoDebugStringWithIndent(value, 0); 333} 334 335std::string GetProtoDebugStringWithIndent(const %(name)s& value, 336 int indent_size) { 337 std::string indent(indent_size, ' '); 338 std::string output = base::StringPrintf("[%%s] {\\n", 339 value.GetTypeName().c_str()); 340""" % {'name': message.name} 341 define_end = """ 342 output += indent + "}\\n"; 343 return output; 344} 345""" 346 singular_field = """ 347 if (value.has_%(name)s()) { 348 output += indent + " %(name)s: "; 349 base::StringAppendF(&output, %(format)s); 350 output += "\\n"; 351 }""" 352 repeated_field = """ 353 output += indent + " %(name)s: {"; 354 for (int i = 0; i < value.%(name)s_size(); ++i) { 355 if (i > 0) { 356 base::StringAppendF(&output, ", "); 357 } 358 base::StringAppendF(&output, %(format)s); 359 } 360 output += "}\\n";""" 361 singular_field_get = 'value.%(name)s()' 362 repeated_field_get = 'value.%(name)s(i)' 363 formats = {'bool': '"%%s", %(value)s ? "true" : "false"', 364 'int32': '"%%d", %(value)s', 365 'int64': '"%%ld", %(value)s', 366 'uint32': '"%%u (0x%%08X)", %(value)s, %(value)s', 367 'uint64': '"%%lu (0x%%016X)", %(value)s, %(value)s', 368 'string': '"%%s", %(value)s.c_str()', 369 'bytes': """"%%s", base::HexEncode(%(value)s.data(), 370 %(value)s.size()).c_str()"""} 371 subtype_format = ('"%%s", GetProtoDebugStringWithIndent(%(value)s, ' 372 'indent_size + 2).c_str()') 373 374 header_file.write(declare) 375 impl_file.write(define_begin) 376 for field in message.fields: 377 if field.repeated: 378 value_get = repeated_field_get % {'name': field.name} 379 field_code = repeated_field 380 else: 381 value_get = singular_field_get % {'name': field.name} 382 field_code = singular_field 383 if field.type_ in formats: 384 value_format = formats[field.type_] % {'value': value_get} 385 else: 386 value_format = subtype_format % {'value': value_get} 387 impl_file.write(field_code % {'name': field.name, 388 'format': value_format}) 389 impl_file.write(define_end) 390 391 392def FormatFile(filename): 393 subprocess.call(['clang-format', '-i', '-style=Chromium', filename]) 394 395 396def main(): 397 parser = argparse.ArgumentParser(description='print proto code generator') 398 parser.add_argument('input_file') 399 parser.add_argument('--subdir', default='') 400 args = parser.parse_args() 401 with open(args.input_file) as input_file: 402 package, imports, messages, enums = ParseProto(input_file) 403 proto_name = os.path.basename(args.input_file).rsplit('.', 1)[0] 404 header_file_name = 'print_%s_proto.h' % proto_name 405 impl_file_name = 'print_%s_proto.cc' % proto_name 406 with open(header_file_name, 'w') as header_file: 407 with open(impl_file_name, 'w') as impl_file: 408 GenerateFileHeaders(proto_name, package, imports, args.subdir, 409 header_file_name, header_file, impl_file) 410 for enum in enums: 411 GenerateEnumPrinter(enum, header_file, impl_file) 412 for message in messages: 413 GenerateMessagePrinter(message, header_file, impl_file) 414 GenerateFileFooters(proto_name, package, args.subdir, header_file, 415 impl_file) 416 FormatFile(header_file_name) 417 FormatFile(impl_file_name) 418 419if __name__ == '__main__': 420 main() 421