Mercurial > hg > orthanc-java
annotate CodeGeneration/ParseOrthancSDK.py @ 43:678bbed285a1 default tip
improved import of JNI in cmake
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Fri, 06 Sep 2024 13:53:54 +0200 |
parents | f8e664baa9dd |
children |
rev | line source |
---|---|
0 | 1 #!/usr/bin/env python3 |
2 | |
18 | 3 # SPDX-FileCopyrightText: 2023-2024 Sebastien Jodogne, UCLouvain, Belgium |
0 | 4 # SPDX-License-Identifier: GPL-3.0-or-later |
5 | |
6 # Java plugin for Orthanc | |
18 | 7 # Copyright (C) 2023-2024 Sebastien Jodogne, UCLouvain, Belgium |
0 | 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 | |
38
f8e664baa9dd
improved statistics in ParseOrthancSDK.py
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
18
diff
changeset
|
435 node.spelling.startswith('OrthancPlugin')): |
f8e664baa9dd
improved statistics in ParseOrthancSDK.py
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
18
diff
changeset
|
436 |
f8e664baa9dd
improved statistics in ParseOrthancSDK.py
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
18
diff
changeset
|
437 if node.spelling in SPECIAL_FUNCTIONS: |
f8e664baa9dd
improved statistics in ParseOrthancSDK.py
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
18
diff
changeset
|
438 countAllFunctions += 1 |
f8e664baa9dd
improved statistics in ParseOrthancSDK.py
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
18
diff
changeset
|
439 continue |
f8e664baa9dd
improved statistics in ParseOrthancSDK.py
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
18
diff
changeset
|
440 |
0 | 441 args = list(filter(lambda x: x.kind == clang.cindex.CursorKind.PARM_DECL, |
442 node.get_children())) | |
443 | |
444 # Check that the first argument is the Orthanc context | |
445 if (len(args) == 0 or | |
446 args[0].type.kind != clang.cindex.TypeKind.POINTER or | |
447 args[0].type.get_pointee().spelling != 'OrthancPluginContext'): | |
448 print('[INFO] Not in the Orthanc SDK: %s()' % node.spelling) | |
449 continue | |
450 | |
451 countAllFunctions += 1 | |
452 | |
453 contextName = args[0].spelling | |
454 args = args[1:] # Skip the Orthanc context | |
455 | |
456 if (len(args) >= 1 and | |
457 args[0].type.spelling in [ 'OrthancPluginMemoryBuffer *', | |
458 'OrthancPluginMemoryBuffer64 *' ]): | |
459 # The method/function returns a byte array | |
460 returnBufferType = args[0].type.spelling | |
461 args = args[1:] | |
462 else: | |
463 returnBufferType = None | |
464 | |
465 if (len(args) >= 1 and | |
466 (IsClassType(args[0].type) or | |
467 IsConstClassType(args[0].type))): | |
468 | |
469 # This is a class method | |
470 cls = args[0].type.get_pointee().spelling | |
471 if IsConstClassType(args[0].type): | |
472 cls = RemovePrefix('const ', cls) | |
473 | |
474 # Special case of destructors | |
475 if (len(args) == 1 and | |
476 not args[0].type.get_pointee().is_const_qualified() and | |
477 node.spelling.startswith('OrthancPluginFree')): | |
478 classes[cls]['destructor'] = node.spelling | |
479 countWrappedFunctions += 1 | |
480 | |
481 else: | |
482 if node.raw_comment == None: | |
483 raise Exception('Method without documentation: %s' % node.spelling) | |
484 | |
485 doc = ParseFunctionDocumentation(node.raw_comment) | |
486 del doc['args'][contextName] # Remove OrthancPluginContext from documentation | |
487 del doc['args'][args[0].spelling] # Remove self from documentation | |
488 | |
489 method = { | |
490 'c_function' : node.spelling, | |
491 'const' : args[0].type.get_pointee().is_const_qualified(), | |
492 'documentation' : doc, | |
493 } | |
494 | |
495 if not EncodeArguments(method, args[1:]): | |
496 pass | |
497 elif EncodeResultType(method, returnBufferType, node.result_type): | |
498 classes[cls]['methods'].append(method) | |
499 countWrappedFunctions += 1 | |
500 else: | |
501 print('[WARNING] Unsupported return type in a method (%s), cannot wrap: %s' % ( | |
502 node.result_type.spelling, node.spelling)) | |
503 | |
504 else: | |
505 # This is a global function | |
506 if node.raw_comment == None: | |
507 raise Exception('Global function without documentation: %s' % node.spelling) | |
508 | |
509 doc = ParseFunctionDocumentation(node.raw_comment) | |
510 del doc['args'][contextName] # Remove OrthancPluginContext from documentation | |
511 | |
512 f = { | |
513 'c_function' : node.spelling, | |
514 'documentation' : doc, | |
515 } | |
516 | |
517 if not EncodeArguments(f, args): | |
518 pass | |
519 elif EncodeResultType(f, returnBufferType, node.result_type): | |
520 globalFunctions.append(f) | |
521 countWrappedFunctions += 1 | |
522 else: | |
523 print('[WARNING] Unsupported return type in a global function (%s), cannot wrap: %s' % ( | |
524 node.result_type.spelling, node.spelling)) | |
525 | |
526 | |
527 | |
528 # Thirdly, export the code model | |
529 | |
530 def FlattenEnumerations(): | |
531 result = [] | |
532 for (name, content) in enumerations.items(): | |
533 result.append({ | |
534 'name' : name, | |
535 'values' : content['values'], | |
536 'documentation' : content['documentation'], | |
537 }) | |
538 return result | |
539 | |
540 def FlattenDictionary(source): | |
541 result = [] | |
542 for (name, value) in source.items(): | |
543 result.append(value) | |
544 return result | |
545 | |
546 codeModel = { | |
547 'classes' : sorted(FlattenDictionary(classes), key = lambda x: x['name']), | |
548 'enumerations' : sorted(FlattenEnumerations(), key = lambda x: x['name']), | |
549 'global_functions' : globalFunctions, # Global functions are ordered in the same order as in the C header | |
550 } | |
551 | |
552 | |
553 with open(TARGET, 'w') as f: | |
554 f.write(json.dumps(codeModel, sort_keys = True, indent = 4)) | |
555 | |
556 print('\nTotal functions in the SDK: %d' % countAllFunctions) | |
38
f8e664baa9dd
improved statistics in ParseOrthancSDK.py
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
18
diff
changeset
|
557 print('Total wrapped functions (including destructors): %d' % countWrappedFunctions) |
0 | 558 print('Coverage: %.0f%%' % (float(countWrappedFunctions) / |
559 float(countAllFunctions) * 100.0)) |