nanopb_objc_generator.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. #!/usr/bin/env python
  2. # Copyright 2021 Google LLC
  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 shlex
  24. from google.protobuf.descriptor_pb2 import FieldDescriptorProto
  25. # The plugin_pb2 package loads descriptors on import, but doesn't defend
  26. # against multiple imports. Reuse the plugin package as loaded by the
  27. # nanopb_generator.
  28. plugin_pb2 = nanopb.plugin_pb2
  29. nanopb_pb2 = nanopb.nanopb_pb2
  30. def main():
  31. # Parse request
  32. data = io.open(sys.stdin.fileno(), 'rb').read()
  33. request = plugin_pb2.CodeGeneratorRequest.FromString(data)
  34. # Preprocess inputs, changing types and nanopb defaults
  35. options = nanopb_parse_options(request)
  36. use_anonymous_oneof(request)
  37. use_bytes_for_strings(request)
  38. # Generate code
  39. parsed_files = nanopb_parse_files(request, options)
  40. results = nanopb_generate(request, options, parsed_files)
  41. response = nanopb_write(results)
  42. # Write to stdout
  43. io.open(sys.stdout.fileno(), 'wb').write(response.SerializeToString())
  44. def use_anonymous_oneof(request):
  45. """Use anonymous unions for oneofs if they're the only one in a message.
  46. Equivalent to setting this option on messages where it applies:
  47. option (nanopb).anonymous_oneof = true;
  48. Args:
  49. request: A CodeGeneratorRequest from protoc. The descriptors are modified
  50. in place.
  51. """
  52. for _, message_type in iterate_messages(request):
  53. if len(message_type.oneof_decl) == 1:
  54. ext = message_type.options.Extensions[nanopb_pb2.nanopb_msgopt]
  55. ext.anonymous_oneof = True
  56. def use_bytes_for_strings(request):
  57. """Always use the bytes type instead of string.
  58. By default, nanopb renders proto strings as having the C type char* and does
  59. not include a separate size field, getting the length of the string via
  60. strlen(). Unfortunately this prevents using strings with embedded nulls,
  61. which is something the wire format supports.
  62. Fortunately, string and bytes proto fields are identical on the wire and
  63. nanopb's bytes representation does have an explicit length, so this function
  64. changes the types of all string fields to bytes. The generated code will now
  65. contain pb_bytes_array_t.
  66. There's no nanopb or proto option to control this behavior. The equivalent
  67. would be to hand edit all the .proto files :-(.
  68. Args:
  69. request: A CodeGeneratorRequest from protoc. The descriptors are modified
  70. in place.
  71. """
  72. for names, message_type in iterate_messages(request):
  73. for field in message_type.field:
  74. if field.type == FieldDescriptorProto.TYPE_STRING:
  75. field.type = FieldDescriptorProto.TYPE_BYTES
  76. def iterate_messages(request):
  77. """Iterates over all messages in all files in the request.
  78. Args:
  79. request: A CodeGeneratorRequest passed by protoc.
  80. Yields:
  81. names: a nanopb.Names object giving a qualified name for the message
  82. message_type: a DescriptorProto for the message.
  83. """
  84. for fdesc in request.proto_file:
  85. for names, message_type in nanopb.iterate_messages(fdesc):
  86. yield names, message_type
  87. def nanopb_parse_options(request):
  88. """Parses nanopb_generator command-line options from the given request.
  89. Args:
  90. request: A CodeGeneratorRequest passed by protoc.
  91. Returns:
  92. Nanopb's options object, obtained via optparser.
  93. """
  94. # Parse options the same as nanopb_generator.main_plugin() does.
  95. args = shlex.split(request.parameter)
  96. options, _ = nanopb.optparser.parse_args(args)
  97. # Force certain options
  98. options.extension = '.nanopb'
  99. options.verbose = True
  100. # Replicate options setup from nanopb_generator.main_plugin.
  101. nanopb.Globals.verbose_options = options.verbose
  102. # Google's protoc does not currently indicate the full path of proto files.
  103. # Instead always add the main file path to the search dirs, that works for
  104. # the common case.
  105. options.options_path.append(os.path.dirname(request.file_to_generate[0]))
  106. return options
  107. def nanopb_parse_files(request, options):
  108. """Parses the files in the given request into nanopb ProtoFile objects.
  109. Args:
  110. request: A CodeGeneratorRequest, as passed by protoc.
  111. options: The command-line options from nanopb_parse_options.
  112. Returns:
  113. A dictionary of filename to nanopb.ProtoFile objects, each one representing
  114. the parsed form of a FileDescriptor in the request.
  115. """
  116. # Process any include files first, in order to have them
  117. # available as dependencies
  118. parsed_files = {}
  119. for fdesc in request.proto_file:
  120. parsed_files[fdesc.name] = nanopb.parse_file(fdesc.name, fdesc, options)
  121. return parsed_files
  122. def nanopb_generate(request, options, parsed_files):
  123. """Generates C sources from the given parsed files.
  124. Args:
  125. request: A CodeGeneratorRequest, as passed by protoc.
  126. options: The command-line options from nanopb_parse_options.
  127. parsed_files: A dictionary of filename to nanopb.ProtoFile, as returned by
  128. nanopb_parse_files().
  129. Returns:
  130. A list of nanopb output dictionaries, each one representing the code
  131. generation result for each file to generate. The output dictionaries have
  132. the following form:
  133. {
  134. 'headername': Name of header file, ending in .h,
  135. 'headerdata': Contents of the header file,
  136. 'sourcename': Name of the source code file, ending in .c,
  137. 'sourcedata': Contents of the source code file
  138. }
  139. """
  140. output = []
  141. for filename in request.file_to_generate:
  142. for fdesc in request.proto_file:
  143. if fdesc.name == filename:
  144. results = nanopb.process_file(filename, fdesc, options, parsed_files)
  145. output.append(results)
  146. return output
  147. def nanopb_write(results):
  148. """Translates nanopb output dictionaries to a CodeGeneratorResponse.
  149. Args:
  150. results: A list of generated source dictionaries, as returned by
  151. nanopb_generate().
  152. Returns:
  153. A CodeGeneratorResponse describing the result of the code generation
  154. process to protoc.
  155. """
  156. response = plugin_pb2.CodeGeneratorResponse()
  157. for result in results:
  158. f = response.file.add()
  159. f.name = result['headername']
  160. f.content = result['headerdata']
  161. f = response.file.add()
  162. f.name = result['sourcename']
  163. f.content = result['sourcedata']
  164. return response
  165. if __name__ == '__main__':
  166. main()