comparison CodeGeneration/ParseOrthancSDK.py @ 0:3ecef5782f2c

initial commit
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 18 Oct 2023 17:59:44 +0200
parents
children 15dc698243ac
comparison
equal deleted inserted replaced
-1:000000000000 0:3ecef5782f2c
1 #!/usr/bin/env python3
2
3 # SPDX-FileCopyrightText: 2023 Sebastien Jodogne, UCLouvain, Belgium
4 # SPDX-License-Identifier: GPL-3.0-or-later
5
6 # Java plugin for Orthanc
7 # Copyright (C) 2023 Sebastien Jodogne, UCLouvain, Belgium
8 #
9 # This program is free software: you can redistribute it and/or
10 # modify it under the terms of the GNU General Public License as
11 # published by the Free Software Foundation, either version 3 of the
12 # License, or (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful, but
15 # WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17 # General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21
22
23 import argparse
24 import clang.cindex
25 import json
26 import os
27 import pprint
28 import pystache
29 import re
30 import sys
31
32
33 ROOT = os.path.dirname(os.path.realpath(sys.argv[0]))
34
35
36 parser = argparse.ArgumentParser(description = 'Parse the Orthanc SDK.')
37 parser.add_argument('--libclang',
38 default = 'libclang-6.0.so.1',
39 help = 'manually provides the path to the libclang shared library')
40 parser.add_argument('--source',
41 default = os.path.join(ROOT, '../Resources/Orthanc/Sdk-1.10.0/orthanc/OrthancCPlugin.h'),
42 help = 'input path to the Orthanc SDK header')
43 parser.add_argument('--target',
44 default = os.path.join(ROOT, 'CodeModel.json'),
45 help = 'target path to store the JSON code model')
46
47 args = parser.parse_args()
48
49 if len(args.libclang) != 0:
50 clang.cindex.Config.set_library_file(args.libclang)
51
52 index = clang.cindex.Index.create()
53
54 tu = index.parse(args.source, [ ])
55
56 TARGET = os.path.realpath(args.target)
57
58
59
60 SPECIAL_FUNCTIONS = [
61 'OrthancPluginCreateMemoryBuffer',
62 'OrthancPluginCreateMemoryBuffer64',
63 'OrthancPluginFreeMemoryBuffer',
64 'OrthancPluginFreeMemoryBuffer64',
65 'OrthancPluginFreeString',
66 ]
67
68
69
70 # First, discover the classes and enumerations
71 classes = {}
72 enumerations = {}
73
74 def ParseDocumentationLines(comment):
75 s = re.sub('^[ ]*/', '', comment)
76 s = re.sub('/[ ]*$', '', s)
77 s = re.sub('<tt>', '"', s)
78 s = re.sub('</tt>', '"', s)
79 return list(map(lambda x: re.sub('[ ]*\*+', '', x).strip(), s.splitlines()))
80
81 def ParseEnumerationDocumentation(comment):
82 result = ''
83 for line in ParseDocumentationLines(comment):
84 if len(line) > 0 and not line.startswith('@'):
85 if len(result) == 0:
86 result = line
87 else:
88 result = result + ' ' + line
89 return result
90
91 def ParseEnumValueDocumentation(comment):
92 m = re.match(r'/\*!<\s*(.*?)\s*\*/$', comment, re.MULTILINE)
93 if m != None:
94 return m.group(1)
95 else:
96 result = ''
97 for line in ParseDocumentationLines(comment):
98 if len(line) > 0:
99 if len(result) == 0:
100 result = line
101 else:
102 result = result + ' ' + line
103 return result.replace('@brief ', '')
104
105 for node in tu.cursor.get_children():
106 # Only consider the Orthanc SDK
107 path = node.location.file.name
108 if os.path.split(path) [-1] != 'OrthancCPlugin.h':
109 continue
110
111 if node.kind == clang.cindex.CursorKind.ENUM_DECL:
112 if node.type.spelling.startswith('OrthancPlugin'):
113 name = node.type.spelling
114
115 if name in enumerations:
116 raise Exception('Enumeration declared twice: %s' % name)
117
118 if node.raw_comment == None:
119 raise Exception('Enumeration without documentation: %s' % name)
120
121 values = []
122 for item in node.get_children():
123 if (item.kind == clang.cindex.CursorKind.ENUM_CONSTANT_DECL and
124 item.spelling.startswith(name + '_')):
125
126 if item.raw_comment == None:
127 raise Exception('Enumeration value without documentation: %s' % item.spelling)
128
129 key = item.spelling[len(name + '_'):]
130 values.append({
131 'key' : key,
132 'value' : item.enum_value,
133 'documentation' : ParseEnumValueDocumentation(item.raw_comment),
134 })
135
136 elif (item.kind == clang.cindex.CursorKind.ENUM_CONSTANT_DECL and
137 item.spelling == '_%s_INTERNAL' % name):
138 pass
139
140 else:
141 raise Exception('Ignoring unknown enumeration item: %s' % item.spelling)
142
143 enumerations[name] = {
144 'values' : values,
145 'documentation' : ParseEnumerationDocumentation(node.raw_comment),
146 }
147
148 elif node.spelling == '': # Unnamed enumeration (presumbaly "_OrthancPluginService")
149 pass
150
151 else:
152 raise Exception('Ignoring unknown enumeration: %s' % node.spelling)
153
154 elif node.kind == clang.cindex.CursorKind.STRUCT_DECL:
155 if (node.spelling.startswith('_OrthancPlugin') and
156 node.spelling.endswith('_t') and
157 node.spelling != '_OrthancPluginContext_t'):
158
159 name = node.spelling[len('_') : -len('_t')]
160 classes[name] = {
161 'name' : name,
162 'methods' : [ ],
163 }
164
165 elif node.spelling in [ '', # This is an internal structure to call Orthanc SDK
166 '_OrthancPluginContext_t' ]:
167 pass
168
169 else:
170 raise Exception('Ignoring unknown structure: %s' % node.spelling)
171
172
173 # Secondly, loop over the C functions and categorize them either as
174 # method, or as global functions
175
176
177 def RemovePrefix(prefix, s):
178 if not s.startswith(prefix):
179 raise Exception('String "%s" does not start with prefix "%s"' % (s, prefix))
180 else:
181 return s[len(prefix):]
182
183
184 def IsClassType(t):
185 return (t.kind == clang.cindex.TypeKind.POINTER and
186 not t.get_pointee().is_const_qualified() and
187 t.get_pointee().spelling in classes)
188
189
190 def IsConstClassType(t):
191 return (t.kind == clang.cindex.TypeKind.POINTER and
192 t.get_pointee().is_const_qualified() and
193 t.get_pointee().spelling.startswith('const ') and
194 t.get_pointee().spelling[len('const '):] in classes)
195
196
197 def EncodeArguments(target, args):
198 assert(type(target) is dict)
199 result = []
200
201 i = 0
202 while i < len(args):
203 arg = {
204 'name' : 'arg%d' % i,
205 'sdk_name' : args[i].spelling,
206 'sdk_type' : args[i].type.spelling,
207 }
208
209 if (i + 1 < len(args) and
210 args[i].type.spelling == 'const void *' and
211 args[i + 1].type.spelling == 'uint32_t'):
212
213 arg['sdk_type'] = 'const_void_pointer_with_size'
214
215 # Skip the size argument
216 i += 1
217
218 elif arg['sdk_type'] in [ 'float',
219 'int32_t',
220 'uint8_t',
221 'uint16_t',
222 'uint32_t',
223 'uint64_t',
224 'const char *',
225 'const void *' ]:
226 pass
227
228 elif arg['sdk_type'] in enumerations:
229 arg['sdk_type'] = 'enumeration'
230 arg['sdk_enumeration'] = args[i].type.spelling
231
232 elif IsClassType(args[i].type):
233 arg['sdk_type'] = 'object'
234 arg['sdk_class'] = args[i].type.get_pointee().spelling
235
236 elif IsConstClassType(args[i].type):
237 arg['sdk_type'] = 'const_object'
238 arg['sdk_class'] = RemovePrefix('const ', args[i].type.get_pointee().spelling)
239
240 else:
241 print('[WARNING] Unsupported argument type in a method (%s), cannot wrap: %s' % (
242 args[i].type.spelling, node.spelling))
243 return False
244
245 result.append(arg)
246 i += 1
247
248 target['args'] = result
249 return True
250
251
252 def EncodeResultType(target, returnBufferType, t):
253 assert(type(target) is dict)
254 assert('args' in target)
255
256 target['return_sdk_type'] = t.spelling
257
258 if returnBufferType != None:
259 target['return_sdk_type'] = returnBufferType
260 return True
261
262 elif target['return_sdk_type'] in [ 'void',
263 'int32_t',
264 'uint32_t',
265 'int64_t',
266 'char *',
267 'const char *' ]:
268 return True
269
270 elif target['return_sdk_type'] in enumerations:
271 target['return_sdk_type'] = 'enumeration'
272 target['return_sdk_enumeration'] = t.spelling
273 return True
274
275 elif IsClassType(t):
276 target['return_sdk_type'] = 'object'
277 target['return_sdk_class'] = t.get_pointee().spelling
278 return True
279
280 else:
281 return False
282
283
284 def ParseFunctionDocumentation(comment):
285 lines = ParseDocumentationLines(comment)
286
287 sections = []
288 currentType = None
289 currentSection = None
290
291 for i in range(len(lines)):
292 if lines[i].find('@') > 0:
293 raise Exception('Character "@" not occurring at the beggining of a documentation paragraph')
294
295 if (len(lines[i]) == 0 and
296 currentType == None):
297 continue
298
299 m = re.match(r'^@([a-z]+)\s*', lines[i])
300
301 if m == None:
302 if currentType == None:
303 print(comment)
304 raise Exception('Documentation does not begin with a "@"')
305
306 assert(currentSection != None)
307 currentSection.append(lines[i])
308 else:
309 if currentType != None:
310 sections.append({
311 'type' : currentType,
312 'lines' : currentSection,
313 })
314
315 currentType = m.group(1)
316 currentSection = [ lines[i][m.span() [1] : ] ]
317
318 if currentType == None:
319 raise Exception('Empty documentation')
320
321 sections.append({
322 'type' : currentType,
323 'lines' : currentSection,
324 })
325
326 for i in range(len(sections)):
327 paragraphs = []
328 lines = sections[i]['lines']
329 currentParagraph = ''
330 for j in range(len(lines)):
331 if len(lines[j]) == 0:
332 if currentParagraph != '':
333 paragraphs.append(currentParagraph)
334 currentParagraph = ''
335 else:
336 if currentParagraph == '':
337 currentParagraph = lines[j]
338 else:
339 currentParagraph = '%s %s' % (currentParagraph, lines[j])
340 if currentParagraph != '':
341 paragraphs.append(currentParagraph)
342
343 sections[i]['paragraphs'] = paragraphs
344
345 documentation = {
346 'args' : {}
347 }
348
349 for i in range(len(sections)):
350 t = sections[i]['type']
351 paragraphs = sections[i]['paragraphs']
352
353 if t == 'brief':
354 if len(paragraphs) < 1:
355 raise Exception('Bad @brief')
356
357 documentation['summary'] = paragraphs[0]
358 documentation['description'] = paragraphs[1:]
359
360 elif t in [ 'return', 'result' ]:
361 if len(paragraphs) != 1:
362 raise Exception('Bad @return')
363
364 documentation['return'] = paragraphs[0]
365
366 elif t == 'param':
367 if len(paragraphs) != 1:
368 raise Exception('Bad @param')
369
370 m = re.match(r'^([a-zA-Z0-9]+)\s+(.+)', paragraphs[0])
371 if m == None:
372 raise Exception('Bad @param')
373
374 key = m.group(1)
375 value = m.group(2)
376 if (len(key) == 0 or
377 len(value) == 0):
378 raise Exception('Bad @param')
379
380 if key in documentation['args']:
381 raise Exception('Twice the same parameter: %s' % key)
382
383 documentation['args'][key] = value
384
385 elif t == 'warning':
386 if not 'description' in documentation:
387 raise Exception('@warning before @summary')
388
389 if len(paragraphs) == 0:
390 raise Exception('Bad @warning')
391
392 for j in range(len(paragraphs)):
393 if j == 0:
394 documentation['description'].append('Warning: %s' % paragraphs[j])
395 else:
396 documentation['description'].append(paragraphs[j])
397
398 elif t == 'note':
399 if not 'description' in documentation:
400 raise Exception('@note before @summary')
401
402 if len(paragraphs) == 0:
403 raise Exception('Bad @note')
404
405 for j in range(len(paragraphs)):
406 if j == 0:
407 documentation['description'].append('Remark: %s' % paragraphs[j])
408 else:
409 documentation['description'].append(paragraphs[j])
410
411 elif t in [
412 'deprecated',
413 'ingroup',
414 'see',
415 ]:
416 pass
417
418 else:
419 raise Exception('Unsupported documentation token: @%s' % t)
420
421 return documentation
422
423
424 globalFunctions = []
425 countWrappedFunctions = 0
426 countAllFunctions = 0
427
428 for node in tu.cursor.get_children():
429 # Only consider the Orthanc SDK
430 path = node.location.file.name
431 if os.path.split(path) [-1] != 'OrthancCPlugin.h':
432 continue
433
434 if (node.kind == clang.cindex.CursorKind.FUNCTION_DECL and
435 node.spelling.startswith('OrthancPlugin') and
436 not node.spelling in SPECIAL_FUNCTIONS):
437 args = list(filter(lambda x: x.kind == clang.cindex.CursorKind.PARM_DECL,
438 node.get_children()))
439
440 # Check that the first argument is the Orthanc context
441 if (len(args) == 0 or
442 args[0].type.kind != clang.cindex.TypeKind.POINTER or
443 args[0].type.get_pointee().spelling != 'OrthancPluginContext'):
444 print('[INFO] Not in the Orthanc SDK: %s()' % node.spelling)
445 continue
446
447 countAllFunctions += 1
448
449 contextName = args[0].spelling
450 args = args[1:] # Skip the Orthanc context
451
452 if (len(args) >= 1 and
453 args[0].type.spelling in [ 'OrthancPluginMemoryBuffer *',
454 'OrthancPluginMemoryBuffer64 *' ]):
455 # The method/function returns a byte array
456 returnBufferType = args[0].type.spelling
457 args = args[1:]
458 else:
459 returnBufferType = None
460
461 if (len(args) >= 1 and
462 (IsClassType(args[0].type) or
463 IsConstClassType(args[0].type))):
464
465 # This is a class method
466 cls = args[0].type.get_pointee().spelling
467 if IsConstClassType(args[0].type):
468 cls = RemovePrefix('const ', cls)
469
470 # Special case of destructors
471 if (len(args) == 1 and
472 not args[0].type.get_pointee().is_const_qualified() and
473 node.spelling.startswith('OrthancPluginFree')):
474 classes[cls]['destructor'] = node.spelling
475 countWrappedFunctions += 1
476
477 else:
478 if node.raw_comment == None:
479 raise Exception('Method without documentation: %s' % node.spelling)
480
481 doc = ParseFunctionDocumentation(node.raw_comment)
482 del doc['args'][contextName] # Remove OrthancPluginContext from documentation
483 del doc['args'][args[0].spelling] # Remove self from documentation
484
485 method = {
486 'c_function' : node.spelling,
487 'const' : args[0].type.get_pointee().is_const_qualified(),
488 'documentation' : doc,
489 }
490
491 if not EncodeArguments(method, args[1:]):
492 pass
493 elif EncodeResultType(method, returnBufferType, node.result_type):
494 classes[cls]['methods'].append(method)
495 countWrappedFunctions += 1
496 else:
497 print('[WARNING] Unsupported return type in a method (%s), cannot wrap: %s' % (
498 node.result_type.spelling, node.spelling))
499
500 else:
501 # This is a global function
502 if node.raw_comment == None:
503 raise Exception('Global function without documentation: %s' % node.spelling)
504
505 doc = ParseFunctionDocumentation(node.raw_comment)
506 del doc['args'][contextName] # Remove OrthancPluginContext from documentation
507
508 f = {
509 'c_function' : node.spelling,
510 'documentation' : doc,
511 }
512
513 if not EncodeArguments(f, args):
514 pass
515 elif EncodeResultType(f, returnBufferType, node.result_type):
516 globalFunctions.append(f)
517 countWrappedFunctions += 1
518 else:
519 print('[WARNING] Unsupported return type in a global function (%s), cannot wrap: %s' % (
520 node.result_type.spelling, node.spelling))
521
522
523
524 # Thirdly, export the code model
525
526 def FlattenEnumerations():
527 result = []
528 for (name, content) in enumerations.items():
529 result.append({
530 'name' : name,
531 'values' : content['values'],
532 'documentation' : content['documentation'],
533 })
534 return result
535
536 def FlattenDictionary(source):
537 result = []
538 for (name, value) in source.items():
539 result.append(value)
540 return result
541
542 codeModel = {
543 'classes' : sorted(FlattenDictionary(classes), key = lambda x: x['name']),
544 'enumerations' : sorted(FlattenEnumerations(), key = lambda x: x['name']),
545 'global_functions' : globalFunctions, # Global functions are ordered in the same order as in the C header
546 }
547
548
549 with open(TARGET, 'w') as f:
550 f.write(json.dumps(codeModel, sort_keys = True, indent = 4))
551
552 print('\nTotal functions in the SDK: %d' % countAllFunctions)
553 print('Total wrapped functions: %d' % countWrappedFunctions)
554 print('Coverage: %.0f%%' % (float(countWrappedFunctions) /
555 float(countAllFunctions) * 100.0))