nanopb_cpp_generator.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. #!/usr/bin/env python
  2. # Copyright 2018 Google
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. """Generates and massages protocol buffer outputs.
  16. """
  17. from __future__ import print_function
  18. import sys
  19. import io
  20. import nanopb_generator as nanopb
  21. import os
  22. import os.path
  23. import re
  24. import shlex
  25. import textwrap
  26. from google.protobuf.descriptor_pb2 import FieldDescriptorProto
  27. from lib import pretty_printing as printing
  28. if sys.platform == 'win32':
  29. import msvcrt # pylint: disable=g-import-not-at-top
  30. # The plugin_pb2 package loads descriptors on import, but doesn't defend
  31. # against multiple imports. Reuse the plugin package as loaded by the
  32. # nanopb_generator.
  33. plugin_pb2 = nanopb.plugin_pb2
  34. nanopb_pb2 = nanopb.nanopb_pb2
  35. def main():
  36. # Parse request
  37. if sys.platform == 'win32':
  38. # Set stdin and stdout to binary mode
  39. msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
  40. msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
  41. data = io.open(sys.stdin.fileno(), 'rb').read()
  42. request = plugin_pb2.CodeGeneratorRequest.FromString(data)
  43. # Preprocess inputs, changing types and nanopb defaults
  44. use_anonymous_oneof(request)
  45. use_bytes_for_strings(request)
  46. use_malloc(request)
  47. # Generate code
  48. options = nanopb_parse_options(request)
  49. parsed_files = nanopb_parse_files(request, options)
  50. results = nanopb_generate(request, options, parsed_files)
  51. pretty_printing = create_pretty_printing(parsed_files)
  52. response = nanopb_write(results, pretty_printing)
  53. # Write to stdout
  54. io.open(sys.stdout.fileno(), 'wb').write(response.SerializeToString())
  55. def use_malloc(request):
  56. """Mark all variable length items as requiring malloc.
  57. By default nanopb renders string, bytes, and repeated fields (dynamic fields)
  58. as having the C type pb_callback_t. Unfortunately this type is incompatible
  59. with nanopb's union support.
  60. The function performs the equivalent of adding the following annotation to
  61. each dynamic field in all the protos.
  62. string name = 1 [(nanopb).type = FT_POINTER];
  63. Args:
  64. request: A CodeGeneratorRequest from protoc. The descriptors are modified
  65. in place.
  66. """
  67. dynamic_types = [
  68. FieldDescriptorProto.TYPE_STRING,
  69. FieldDescriptorProto.TYPE_BYTES,
  70. ]
  71. for _, message_type in iterate_messages(request):
  72. for field in message_type.field:
  73. dynamic_type = field.type in dynamic_types
  74. repeated = field.label == FieldDescriptorProto.LABEL_REPEATED
  75. if dynamic_type or repeated:
  76. ext = field.options.Extensions[nanopb_pb2.nanopb]
  77. ext.type = nanopb_pb2.FT_POINTER
  78. def use_anonymous_oneof(request):
  79. """Use anonymous unions for oneofs if they're the only one in a message.
  80. Equivalent to setting this option on messages where it applies:
  81. option (nanopb).anonymous_oneof = true;
  82. Args:
  83. request: A CodeGeneratorRequest from protoc. The descriptors are modified
  84. in place.
  85. """
  86. for _, message_type in iterate_messages(request):
  87. if len(message_type.oneof_decl) == 1:
  88. ext = message_type.options.Extensions[nanopb_pb2.nanopb_msgopt]
  89. ext.anonymous_oneof = True
  90. def use_bytes_for_strings(request):
  91. """Always use the bytes type instead of string.
  92. By default, nanopb renders proto strings as having the C type char* and does
  93. not include a separate size field, getting the length of the string via
  94. strlen(). Unfortunately this prevents using strings with embedded nulls,
  95. which is something the wire format supports.
  96. Fortunately, string and bytes proto fields are identical on the wire and
  97. nanopb's bytes representation does have an explicit length, so this function
  98. changes the types of all string fields to bytes. The generated code will now
  99. contain pb_bytes_array_t.
  100. There's no nanopb or proto option to control this behavior. The equivalent
  101. would be to hand edit all the .proto files :-(.
  102. Args:
  103. request: A CodeGeneratorRequest from protoc. The descriptors are modified
  104. in place.
  105. """
  106. for names, message_type in iterate_messages(request):
  107. for field in message_type.field:
  108. if field.type == FieldDescriptorProto.TYPE_STRING:
  109. field.type = FieldDescriptorProto.TYPE_BYTES
  110. def iterate_messages(request):
  111. """Iterates over all messages in all files in the request.
  112. Args:
  113. request: A CodeGeneratorRequest passed by protoc.
  114. Yields:
  115. names: a nanopb.Names object giving a qualified name for the message
  116. message_type: a DescriptorProto for the message.
  117. """
  118. for fdesc in request.proto_file:
  119. for names, message_type in nanopb.iterate_messages(fdesc):
  120. yield names, message_type
  121. def nanopb_parse_options(request):
  122. """Parses nanopb_generator command-line options from the given request.
  123. Args:
  124. request: A CodeGeneratorRequest passed by protoc.
  125. Returns:
  126. Nanopb's options object, obtained via optparser.
  127. """
  128. # Parse options the same as nanopb_generator.main_plugin() does.
  129. args = shlex.split(request.parameter)
  130. options, _ = nanopb.optparser.parse_args(args)
  131. # Force certain options
  132. options.extension = '.nanopb'
  133. # Replicate options setup from nanopb_generator.main_plugin.
  134. nanopb.Globals.verbose_options = options.verbose
  135. # Google's protoc does not currently indicate the full path of proto files.
  136. # Instead always add the main file path to the search dirs, that works for
  137. # the common case.
  138. options.options_path.append(os.path.dirname(request.file_to_generate[0]))
  139. return options
  140. def nanopb_parse_files(request, options):
  141. """Parses the files in the given request into nanopb ProtoFile objects.
  142. Args:
  143. request: A CodeGeneratorRequest, as passed by protoc.
  144. options: The command-line options from nanopb_parse_options.
  145. Returns:
  146. A dictionary of filename to nanopb.ProtoFile objects, each one representing
  147. the parsed form of a FileDescriptor in the request.
  148. """
  149. # Process any include files first, in order to have them available as
  150. # dependencies
  151. parsed_files = {}
  152. for fdesc in request.proto_file:
  153. parsed_files[fdesc.name] = nanopb.parse_file(fdesc.name, fdesc, options)
  154. return parsed_files
  155. def create_pretty_printing(parsed_files):
  156. """Creates a `FilePrettyPrinting` for each of the given files.
  157. Args:
  158. parsed_files: A dictionary of proto file names (e.g. `foo/bar/baz.proto`) to
  159. `nanopb.ProtoFile` descriptors.
  160. Returns:
  161. A dictionary of short (without extension) proto file names (e.g.,
  162. `foo/bar/baz`) to `FilePrettyPrinting` objects.
  163. """
  164. pretty_printing = {}
  165. for name, parsed_file in parsed_files.items():
  166. base_filename = name.replace('.proto', '')
  167. pretty_printing[base_filename] = printing.FilePrettyPrinting(parsed_file)
  168. return pretty_printing
  169. def nanopb_generate(request, options, parsed_files):
  170. """Generates C sources from the given parsed files.
  171. Args:
  172. request: A CodeGeneratorRequest, as passed by protoc.
  173. options: The command-line options from nanopb_parse_options.
  174. parsed_files: A dictionary of filename to nanopb.ProtoFile, as returned by
  175. nanopb_parse_files().
  176. Returns:
  177. A list of nanopb output dictionaries, each one representing the code
  178. generation result for each file to generate. The output dictionaries have
  179. the following form:
  180. {
  181. 'headername': Name of header file, ending in .h,
  182. 'headerdata': Contents of the header file,
  183. 'sourcename': Name of the source code file, ending in .c,
  184. 'sourcedata': Contents of the source code file
  185. }
  186. """
  187. output = []
  188. for filename in request.file_to_generate:
  189. for fdesc in request.proto_file:
  190. if fdesc.name == filename:
  191. results = nanopb.process_file(filename, fdesc, options, parsed_files)
  192. output.append(results)
  193. return output
  194. def nanopb_write(results, pretty_printing):
  195. """Translates nanopb output dictionaries to a CodeGeneratorResponse.
  196. Args:
  197. results: A list of generated source dictionaries, as returned by
  198. nanopb_generate().
  199. file_pretty_printing: A dictionary of `FilePrettyPrinting` objects, indexed
  200. by short file name (without extension).
  201. Returns:
  202. A CodeGeneratorResponse describing the result of the code generation
  203. process to protoc.
  204. """
  205. response = plugin_pb2.CodeGeneratorResponse()
  206. for result in results:
  207. base_filename = result['headername'].replace('.nanopb.h', '')
  208. file_pretty_printing = pretty_printing[base_filename]
  209. generated_header = GeneratedFile(response.file, result['headername'],
  210. nanopb_fixup(result['headerdata']))
  211. nanopb_augment_header(generated_header, file_pretty_printing)
  212. generated_source = GeneratedFile(response.file, result['sourcename'],
  213. nanopb_fixup(result['sourcedata']))
  214. nanopb_augment_source(generated_source, file_pretty_printing)
  215. return response
  216. class GeneratedFile:
  217. """Represents a request to generate a file.
  218. The initial contents of the file can be augmented by inserting extra text at
  219. insertion points. For each file, Nanopb defines the following insertion
  220. points (each marked `@@protoc_insertion_point`):
  221. - 'includes' -- beginning of the file, after the last Nanopb include;
  222. - 'eof' -- the very end of file, right before the include guard.
  223. In addition, each header also defines a 'struct:Foo' insertion point inside
  224. each struct declaration, where 'Foo' is the name of the struct.
  225. See the official protobuf docs for more information on insertion points:
  226. https://github.com/protocolbuffers/protobuf/blob/129a7c875fc89309a2ab2fbbc940268bbf42b024/src/google/protobuf/compiler/plugin.proto#L125-L162
  227. """
  228. def __init__(self, files, file_name, contents):
  229. """
  230. Args:
  231. files: The array of files to generate inside a `CodeGenerationResponse`.
  232. New files will be added to it.
  233. file_name: The name of the file to generate/augment.
  234. contents: The initial contents of the file, before any augmentation, as
  235. a single string.
  236. """
  237. self.files = files
  238. self.file_name = file_name
  239. self._set_contents(contents)
  240. def _set_contents(self, contents):
  241. """Creates a request to generate a new file with the given `contents`.
  242. """
  243. f = self.files.add()
  244. f.name = self.file_name
  245. f.content = contents
  246. def insert(self, insertion_point, to_insert):
  247. """Adds extra text to the generated file at the given `insertion_point`.
  248. Args:
  249. insertion_point: The string identifier of the insertion point, e.g. 'eof'.
  250. The extra text will be inserted right before the insertion point. If
  251. `insert` is called repeatedly, insertions will be added in the order of
  252. the calls. All possible insertion points are defined by Nanopb; see the
  253. class comment for additional details.
  254. to_insert: The text to insert as a string.
  255. """
  256. f = self.files.add()
  257. f.name = self.file_name
  258. f.insertion_point = insertion_point
  259. f.content = to_insert
  260. def nanopb_fixup(file_contents):
  261. """Applies fixups to generated Nanopb code.
  262. This is for changes to the code, as well as additions that cannot be made via
  263. insertion points. Current fixups:
  264. - rename fields named `delete` to `delete_`, because it's a keyword in C++.
  265. Args:
  266. file_contents: The contents of the generated file as a single string. The
  267. fixups will be applied without distinguishing between the code and the
  268. comments.
  269. """
  270. delete_keyword = re.compile(r'\bdelete\b')
  271. return delete_keyword.sub('delete_', file_contents)
  272. def nanopb_augment_header(generated_header, file_pretty_printing):
  273. """Augments a `.h` generated file with pretty-printing support.
  274. Also puts all code in `firebase::firestore` namespace.
  275. Args:
  276. generated_header: The `.h` file that will be generated.
  277. file_pretty_printing: `FilePrettyPrinting` for this header.
  278. """
  279. generated_header.insert('includes', '#include <string>\n\n')
  280. open_namespace(generated_header)
  281. for e in file_pretty_printing.enums:
  282. generated_header.insert('eof', e.generate_declaration())
  283. for m in file_pretty_printing.messages:
  284. generated_header.insert('struct:' + m.full_classname, m.generate_declaration())
  285. close_namespace(generated_header)
  286. def nanopb_augment_source(generated_source, file_pretty_printing):
  287. """Augments a `.cc` generated file with pretty-printing support.
  288. Also puts all code in `firebase::firestore` namespace.
  289. Args:
  290. generated_source: The `.cc` file that will be generated.
  291. file_pretty_printing: `FilePrettyPrinting` for this source.
  292. """
  293. generated_source.insert('includes', textwrap.dedent('\
  294. #include "Firestore/core/src/nanopb/pretty_printing.h"\n\n'))
  295. open_namespace(generated_source)
  296. add_using_declarations(generated_source)
  297. for e in file_pretty_printing.enums:
  298. generated_source.insert('eof', e.generate_definition())
  299. for m in file_pretty_printing.messages:
  300. generated_source.insert('eof', m.generate_definition())
  301. close_namespace(generated_source)
  302. def open_namespace(generated_file):
  303. """Augments a generated file by opening the `f::f` namespace.
  304. """
  305. generated_file.insert('includes', textwrap.dedent('''\
  306. namespace firebase {
  307. namespace firestore {\n\n'''))
  308. def close_namespace(generated_file):
  309. """Augments a generated file by closing the `f::f` namespace.
  310. """
  311. generated_file.insert('eof', textwrap.dedent('''\
  312. } // namespace firestore
  313. } // namespace firebase\n\n'''))
  314. def add_using_declarations(generated_file):
  315. """Augments a generated file by adding the necessary using declarations.
  316. """
  317. generated_file.insert('includes', '''\
  318. using nanopb::PrintEnumField;
  319. using nanopb::PrintHeader;
  320. using nanopb::PrintMessageField;
  321. using nanopb::PrintPrimitiveField;
  322. using nanopb::PrintTail;\n\n''');
  323. if __name__ == '__main__':
  324. main()