__init__.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. #
  2. # UAVCAN DSDL compiler for libcanard
  3. #
  4. # This code is written by Pavel Kirienko for libuavcan DSDL generator
  5. # copied and modified for the libcanard use
  6. #
  7. # Copyright (C) 2014 Pavel Kirienko <pavel.kirienko@gmail.com>
  8. # Copyright (C) 2018 Intel Corporation
  9. #
  10. '''
  11. This module implements the core functionality of the UAVCAN DSDL compiler for libcanard.
  12. Supported Python versions: 3.2+, 2.7.
  13. It accepts a list of root namespaces and produces the set of C header files and souce files for libcanard.
  14. It is based on the DSDL parsing package from pyuavcan.
  15. '''
  16. from __future__ import division, absolute_import, print_function, unicode_literals
  17. import sys, os, logging, errno, re
  18. from .pyratemp import Template
  19. from uavcan import dsdl
  20. # Python 2.7 compatibility
  21. try:
  22. str = unicode
  23. except NameError:
  24. pass
  25. OUTPUT_HEADER_FILE_EXTENSION = 'h'
  26. OUTPUT_CODE_FILE_EXTENSION = 'c'
  27. HEADER_TEMPLATE_FILENAME = os.path.join(os.path.dirname(__file__), 'data_type_template.tmpl')
  28. CODE_TEMPLATE_FILENAME = os.path.join(os.path.dirname(__file__), 'code_type_template.tmpl')
  29. __all__ = ['run', 'logger', 'DsdlCompilerException']
  30. class DsdlCompilerException(Exception):
  31. pass
  32. logger = logging.getLogger(__name__)
  33. def run(source_dirs, include_dirs, output_dir, header_only):
  34. '''
  35. This function takes a list of root namespace directories (containing DSDL definition files to parse), a
  36. possibly empty list of search directories (containing DSDL definition files that can be referenced from the types
  37. that are going to be parsed), and the output directory path (possibly nonexistent) where the generated C++
  38. header files will be stored.
  39. Note that this module features lazy write, i.e. if an output file does already exist and its content is not going
  40. to change, it will not be overwritten. This feature allows to avoid unnecessary recompilation of dependent object
  41. files.
  42. Args:
  43. source_dirs List of root namespace directories to parse.
  44. include_dirs List of root namespace directories with referenced types (possibly empty). This list is
  45. automaitcally extended with source_dirs.
  46. output_dir Output directory path. Will be created if doesn't exist.
  47. header_only Weather to generated as header only library.
  48. '''
  49. assert isinstance(source_dirs, list)
  50. assert isinstance(include_dirs, list)
  51. output_dir = str(output_dir)
  52. types = run_parser(source_dirs, include_dirs + source_dirs)
  53. if not types:
  54. die('No type definitions were found')
  55. logger.info('%d types total', len(types))
  56. run_generator(types, output_dir, header_only)
  57. # -----------------
  58. def pretty_filename(filename):
  59. try:
  60. a = os.path.abspath(filename)
  61. r = os.path.relpath(filename)
  62. return a if '..' in r else r
  63. except ValueError:
  64. return filename
  65. # get the CamelCase prefix from the current filename
  66. def get_name_space_prefix(t):
  67. return t.full_name.replace('.', '_')
  68. def type_output_filename(t, extension = OUTPUT_HEADER_FILE_EXTENSION):
  69. assert t.category == t.CATEGORY_COMPOUND
  70. folder_name = t.full_name.split('.')[-2]
  71. if extension == OUTPUT_CODE_FILE_EXTENSION:
  72. name_list = t.full_name.split('.')
  73. if len(folder_name):
  74. name_list[-1] = str(folder_name) + '_' + str(name_list[-1])
  75. return os.path.sep.join(name_list) + '.' + extension
  76. else:
  77. return t.full_name.replace('.', os.path.sep) + '.' + extension
  78. def makedirs(path):
  79. try:
  80. try:
  81. os.makedirs(path, exist_ok=True) # May throw "File exists" when executed as root, which is wrong
  82. except TypeError:
  83. os.makedirs(path) # Python 2.7 compatibility
  84. except OSError as ex:
  85. if ex.errno != errno.EEXIST: # http://stackoverflow.com/questions/12468022
  86. raise
  87. def die(text):
  88. raise DsdlCompilerException(str(text))
  89. def run_parser(source_dirs, search_dirs):
  90. try:
  91. types = dsdl.parse_namespaces(source_dirs, search_dirs)
  92. except dsdl.DsdlException as ex:
  93. logger.info('Parser failure', exc_info=True)
  94. die(ex)
  95. return types
  96. def run_generator(types, dest_dir, header_only):
  97. try:
  98. header_template_expander = make_template_expander(HEADER_TEMPLATE_FILENAME)
  99. code_template_expander = make_template_expander(CODE_TEMPLATE_FILENAME)
  100. dest_dir = os.path.abspath(dest_dir) # Removing '..'
  101. makedirs(dest_dir)
  102. for t in types:
  103. logger.info('Generating type %s', t.full_name)
  104. header_path_file_name = os.path.join(dest_dir, type_output_filename(t, OUTPUT_HEADER_FILE_EXTENSION))
  105. code_filename = os.path.join(dest_dir, type_output_filename(t, OUTPUT_CODE_FILE_EXTENSION))
  106. t.header_filename = type_output_filename(t, OUTPUT_HEADER_FILE_EXTENSION)
  107. t.name_space_prefix = get_name_space_prefix(t)
  108. t.header_only = header_only
  109. header_text = generate_one_type(header_template_expander, t)
  110. code_text = generate_one_type(code_template_expander, t)
  111. write_generated_data(header_path_file_name, header_text, header_only)
  112. if header_only:
  113. code_text = "\r\n" + code_text
  114. write_generated_data(header_path_file_name, code_text, header_only, True)
  115. else:
  116. write_generated_data(code_filename, code_text, header_only)
  117. except Exception as ex:
  118. logger.info('Generator failure', exc_info=True)
  119. die(ex)
  120. def write_generated_data(filename, data, header_only, append_file=False):
  121. dirname = os.path.dirname(filename)
  122. makedirs(dirname)
  123. if append_file:
  124. with open(filename, 'a') as f:
  125. f.write(data)
  126. else:
  127. if os.path.exists(filename):
  128. os.remove(filename)
  129. with open(filename, 'w') as f:
  130. f.write(data)
  131. def expand_to_next_full(size):
  132. if size <= 8:
  133. return 8
  134. elif size <= 16:
  135. return 16
  136. elif size <= 32:
  137. return 32
  138. elif size <=64:
  139. return 64
  140. def get_max_size(bits, unsigned):
  141. if unsigned:
  142. return (2 ** bits) -1
  143. else:
  144. return (2 ** (bits-1)) -1
  145. def strip_name(name):
  146. return name.split('.')[-1]
  147. def type_to_c_type(t):
  148. if t.category == t.CATEGORY_PRIMITIVE:
  149. saturate = {
  150. t.CAST_MODE_SATURATED: True,
  151. t.CAST_MODE_TRUNCATED: False,
  152. }[t.cast_mode]
  153. cast_mode = {
  154. t.CAST_MODE_SATURATED: 'Saturate',
  155. t.CAST_MODE_TRUNCATED: 'Truncate',
  156. }[t.cast_mode]
  157. if t.kind == t.KIND_FLOAT:
  158. float_type = {
  159. 16: 'float',
  160. 32: 'float',
  161. 64: 'double',
  162. }[t.bitlen]
  163. return {'cpp_type':'%s' % (float_type),
  164. 'post_cpp_type':'',
  165. 'cpp_type_comment':'float%d %s' % (t.bitlen, cast_mode, ),
  166. 'bitlen':t.bitlen,
  167. 'max_size':get_max_size(t.bitlen, False),
  168. 'signedness':'false',
  169. 'saturate':False} # do not saturate floats
  170. else:
  171. c_type = {
  172. t.KIND_BOOLEAN: 'bool',
  173. t.KIND_UNSIGNED_INT: 'uint',
  174. t.KIND_SIGNED_INT: 'int',
  175. }[t.kind]
  176. signedness = {
  177. t.KIND_BOOLEAN: 'false',
  178. t.KIND_UNSIGNED_INT: 'false',
  179. t.KIND_SIGNED_INT: 'true',
  180. }[t.kind]
  181. if t.kind == t.KIND_BOOLEAN:
  182. return {'cpp_type':'%s' % (c_type),
  183. 'post_cpp_type':'',
  184. 'cpp_type_comment':'bit len %d' % (t.bitlen, ),
  185. 'bitlen':t.bitlen,
  186. 'max_size':get_max_size(t.bitlen, True),
  187. 'signedness':signedness,
  188. 'saturate':saturate}
  189. else:
  190. if saturate:
  191. # Do not staturate if struct field length is equal bitlen
  192. if (expand_to_next_full(t.bitlen) == t.bitlen):
  193. saturate = False
  194. return {'cpp_type':'%s%d_t' % (c_type, expand_to_next_full(t.bitlen)),
  195. 'post_cpp_type':'',
  196. 'cpp_type_comment':'bit len %d' % (t.bitlen, ),
  197. 'bitlen':t.bitlen,
  198. 'max_size':get_max_size(t.bitlen, t.kind == t.KIND_UNSIGNED_INT),
  199. 'signedness':signedness,
  200. 'saturate':saturate}
  201. elif t.category == t.CATEGORY_ARRAY:
  202. values = type_to_c_type(t.value_type)
  203. mode = {
  204. t.MODE_STATIC: 'Static Array',
  205. t.MODE_DYNAMIC: 'Dynamic Array',
  206. }[t.mode]
  207. return {'cpp_type':'%s' % (values['cpp_type'], ),
  208. 'cpp_type_category': t.value_type.category,
  209. 'post_cpp_type':'[%d]' % (t.max_size,),
  210. 'cpp_type_comment':'%s %dbit[%d] max items' % (mode, values['bitlen'], t.max_size, ),
  211. 'bitlen':values['bitlen'],
  212. 'array_max_size_bit_len':t.max_size.bit_length(),
  213. 'max_size':values['max_size'],
  214. 'signedness':values['signedness'],
  215. 'saturate':values['saturate'],
  216. 'dynamic_array': t.mode == t.MODE_DYNAMIC,
  217. 'max_array_elements': t.max_size,
  218. }
  219. elif t.category == t.CATEGORY_COMPOUND:
  220. return {
  221. 'cpp_type':t.full_name.replace('.','_'),
  222. 'post_cpp_type':'',
  223. 'cpp_type_comment':'',
  224. 'bitlen':t.get_max_bitlen(),
  225. 'max_size':0,
  226. 'signedness':'false',
  227. 'saturate':False}
  228. elif t.category == t.CATEGORY_VOID:
  229. return {'cpp_type':'',
  230. 'post_cpp_type':'',
  231. 'cpp_type_comment':'void%d' % t.bitlen,
  232. 'bitlen':t.bitlen,
  233. 'max_size':0,
  234. 'signedness':'false',
  235. 'saturate':False}
  236. else:
  237. raise DsdlCompilerException('Unknown type category: %s' % t.category)
  238. def generate_one_type(template_expander, t):
  239. t.name_space_type_name = get_name_space_prefix(t)
  240. t.cpp_full_type_name = '::' + t.full_name.replace('.', '::')
  241. t.include_guard = '__' + t.full_name.replace('.', '_').upper()
  242. t.macro_name = t.full_name.replace('.', '_').upper()
  243. # Dependencies (no duplicates)
  244. def fields_includes(fields):
  245. def detect_include(t):
  246. if t.category == t.CATEGORY_COMPOUND:
  247. return type_output_filename(t)
  248. if t.category == t.CATEGORY_ARRAY:
  249. return detect_include(t.value_type)
  250. return list(sorted(set(filter(None, [detect_include(x.type) for x in fields]))))
  251. if t.kind == t.KIND_MESSAGE:
  252. t.cpp_includes = fields_includes(t.fields)
  253. else:
  254. t.cpp_includes = fields_includes(t.request_fields + t.response_fields)
  255. t.cpp_namespace_components = t.full_name.split('.')[:-1]
  256. t.has_default_dtid = t.default_dtid is not None
  257. # Attribute types
  258. def inject_cpp_types(attributes):
  259. length = len(attributes)
  260. count = 0
  261. has_array = False
  262. for a in attributes:
  263. count = count + 1
  264. a.last_item = False
  265. if (count == length):
  266. a.last_item = True
  267. data = type_to_c_type(a.type)
  268. for key, value in data.items():
  269. setattr(a, key, value)
  270. if a.type.category == t.CATEGORY_ARRAY:
  271. a.array_size = a.type.max_size
  272. has_array = True
  273. a.type_category = a.type.category
  274. a.void = a.type.category == a.type.CATEGORY_VOID
  275. if a.void:
  276. assert not a.name
  277. a.name = ''
  278. return has_array
  279. def has_float16(attributes):
  280. has_float16 = False
  281. for a in attributes:
  282. if a.type.category == t.CATEGORY_PRIMITIVE and a.type.kind == a.type.KIND_FLOAT and a.bitlen == 16:
  283. has_float16 = True
  284. return has_float16
  285. if t.kind == t.KIND_MESSAGE:
  286. t.has_array = inject_cpp_types(t.fields)
  287. t.has_float16 = has_float16(t.fields)
  288. inject_cpp_types(t.constants)
  289. t.all_attributes = t.fields + t.constants
  290. t.union = t.union and len(t.fields)
  291. if t.union:
  292. t.union = len(t.fields).bit_length()
  293. else:
  294. t.request_has_array = inject_cpp_types(t.request_fields)
  295. t.request_has_float16 = has_float16(t.request_fields)
  296. inject_cpp_types(t.request_constants)
  297. t.response_has_array = inject_cpp_types(t.response_fields)
  298. t.response_has_float16 = has_float16(t.response_fields)
  299. inject_cpp_types(t.response_constants)
  300. t.all_attributes = t.request_fields + t.request_constants + t.response_fields + t.response_constants
  301. t.request_union = t.request_union and len(t.request_fields)
  302. t.response_union = t.response_union and len(t.response_fields)
  303. if t.request_union:
  304. t.request_union = len(t.request_fields).bit_length()
  305. if t.response_union:
  306. t.response_union = len(t.response_fields).bit_length()
  307. # Constant properties
  308. def inject_constant_info(constants):
  309. for c in constants:
  310. if c.type.kind == c.type.KIND_FLOAT:
  311. float(c.string_value) # Making sure that this is a valid float literal
  312. c.cpp_value = c.string_value
  313. else:
  314. int(c.string_value) # Making sure that this is a valid integer literal
  315. c.cpp_value = c.string_value
  316. if c.type.kind == c.type.KIND_UNSIGNED_INT:
  317. c.cpp_value += 'U'
  318. if t.kind == t.KIND_MESSAGE:
  319. inject_constant_info(t.constants)
  320. else:
  321. inject_constant_info(t.request_constants)
  322. inject_constant_info(t.response_constants)
  323. # Data type kind
  324. t.cpp_kind = {
  325. t.KIND_MESSAGE: '::uavcan::DataTypeKindMessage',
  326. t.KIND_SERVICE: '::uavcan::DataTypeKindService',
  327. }[t.kind]
  328. # Generation
  329. text = template_expander(t=t) # t for Type
  330. text = '\n'.join(x.rstrip() for x in text.splitlines())
  331. text = text.replace('\n\n\n\n\n', '\n\n').replace('\n\n\n\n', '\n\n').replace('\n\n\n', '\n\n')
  332. text = text.replace('{\n\n ', '{\n ')
  333. return text
  334. def make_template_expander(filename):
  335. '''
  336. Templating is based on pyratemp (http://www.simple-is-better.org/template/pyratemp.html).
  337. The pyratemp's syntax is rather verbose and not so human friendly, so we define some
  338. custom extensions to make it easier to read and write.
  339. The resulting syntax somewhat resembles Mako (which was used earlier instead of pyratemp):
  340. Substitution:
  341. ${expression}
  342. Line joining through backslash (replaced with a single space):
  343. ${foo(bar(very_long_arument=42, \
  344. second_line=72))}
  345. Blocks:
  346. % for a in range(10):
  347. % if a == 5:
  348. ${foo()}
  349. % endif
  350. % endfor
  351. The extended syntax is converted into pyratemp's through regexp substitution.
  352. '''
  353. with open(filename) as f:
  354. template_text = f.read()
  355. # Backslash-newline elimination
  356. template_text = re.sub(r'\\\r{0,1}\n\ *', r' ', template_text)
  357. # Substitution syntax transformation: ${foo} ==> $!foo!$
  358. template_text = re.sub(r'([^\$]{0,1})\$\{([^\}]+)\}', r'\1$!\2!$', template_text)
  359. # Flow control expression transformation: % foo: ==> <!--(foo)-->
  360. template_text = re.sub(r'(?m)^(\ *)\%\ *(.+?):{0,1}$', r'\1<!--(\2)-->', template_text)
  361. # Block termination transformation: <!--(endfoo)--> ==> <!--(end)-->
  362. template_text = re.sub(r'\<\!--\(end[a-z]+\)--\>', r'<!--(end)-->', template_text)
  363. # Pyratemp workaround.
  364. # The problem is that if there's no empty line after a macro declaration, first line will be doubly indented.
  365. # Workaround:
  366. # 1. Remove trailing comments
  367. # 2. Add a newline after each macro declaration
  368. template_text = re.sub(r'\ *\#\!.*', '', template_text)
  369. template_text = re.sub(r'(\<\!--\(macro\ [a-zA-Z0-9_]+\)--\>.*?)', r'\1\n', template_text)
  370. # Preprocessed text output for debugging
  371. # with open(filename + '.d', 'w') as f:
  372. # f.write(template_text)
  373. template = Template(template_text)
  374. def expand(**args):
  375. # This function adds one indentation level (4 spaces); it will be used from the template
  376. args['indent'] = lambda text, idnt = ' ': idnt + text.replace('\n', '\n' + idnt)
  377. # This function works like enumerate(), telling you whether the current item is the last one
  378. def enum_last_value(iterable, start=0):
  379. it = iter(iterable)
  380. count = start
  381. last = next(it)
  382. for val in it:
  383. yield count, False, last
  384. last = val
  385. count += 1
  386. yield count, True, last
  387. args['enum_last_value'] = enum_last_value
  388. return template(**args)
  389. return expand