comparison CodeAnalysis/ParseOrthancSDK.py @ 0:7ed502b17b8f

initial commit
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 26 Mar 2020 18:47:01 +0100
parents
children df7b4f8a0437
comparison
equal deleted inserted replaced
-1:000000000000 0:7ed502b17b8f
1 #!/usr/bin/env python
2
3 ##
4 ## Python plugin for Orthanc
5 ## Copyright (C) 2017-2020 Osimis S.A., Belgium
6 ##
7 ## This program is free software: you can redistribute it and/or
8 ## modify it under the terms of the GNU Affero General Public License
9 ## as published by the Free Software Foundation, either version 3 of
10 ## the License, or (at your option) any later version.
11 ##
12 ## This program is distributed in the hope that it will be useful, but
13 ## WITHOUT ANY WARRANTY; without even the implied warranty of
14 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 ## Affero General Public License for more details.
16 ##
17 ## You should have received a copy of the GNU Affero General Public License
18 ## along with this program. If not, see <http://www.gnu.org/licenses/>.
19 ##
20
21
22 # Ubuntu 18.04:
23 # sudo apt-get install python-clang-4.0
24 # python2 ./ParseOrthancSDK.py --libclang=libclang-4.0.so.1 ../Resources/Orthanc/Sdk-1.5.7/orthanc/OrthancCPlugin.h ../Sources/Autogenerated
25
26
27 import argparse
28 import clang.cindex
29 import os
30 import pprint
31 import pystache
32 import sys
33
34
35 ROOT = os.path.dirname(os.path.realpath(sys.argv[0]))
36
37
38 ##
39 ## Parse the command-line arguments
40 ##
41
42 parser = argparse.ArgumentParser(description = 'Parse the Orthanc SDK.')
43 parser.add_argument('--libclang',
44 default = 'libclang-4.0.so.1',
45 help = 'manually provides the path to the libclang shared library')
46 parser.add_argument('--source',
47 default = os.path.join(os.path.dirname(__file__),
48 '../Resources/Orthanc/Sdk-1.5.7/orthanc/OrthancCPlugin.h'),
49 help = 'Input C++ file')
50 parser.add_argument('--target',
51 default = os.path.join(os.path.dirname(__file__),
52 '../Sources/Autogenerated'),
53 help = 'Target folder')
54
55 args = parser.parse_args()
56
57
58
59 if len(args.libclang) != 0:
60 clang.cindex.Config.set_library_file(args.libclang)
61
62 index = clang.cindex.Index.create()
63
64 tu = index.parse(args.source, [ ])
65
66 TARGET = os.path.realpath(args.target)
67
68
69
70 def ToUpperCase(name):
71 s = ''
72 for i in range(len(name)):
73 if name[i].isupper():
74 if len(s) == 0:
75 s += name[i]
76 elif name[i - 1].islower():
77 s += '_' + name[i]
78 elif (i + 1 < len(name) and
79 name[i - 1].islower() and
80 name[i + 1].isupper()):
81 s += '_' + name[i]
82 else:
83 s += name[i]
84 else:
85 s += name[i].upper()
86 return s
87
88
89
90 with open(os.path.join(ROOT, 'Enumeration.mustache'), 'r') as f:
91 TEMPLATE = f.read()
92
93
94 classes = {}
95 enumerations = {}
96 globalFunctions = []
97
98 def IsSourceStringType(t):
99 return (t.kind == clang.cindex.TypeKind.POINTER and
100 t.get_pointee().kind == clang.cindex.TypeKind.CHAR_S and
101 t.get_pointee().is_const_qualified())
102
103 def IsTargetStaticStringType(t):
104 return (t.kind == clang.cindex.TypeKind.POINTER and
105 t.get_pointee().kind == clang.cindex.TypeKind.CHAR_S and
106 t.get_pointee().is_const_qualified())
107
108 def IsTargetDynamicStringType(t):
109 return (t.kind == clang.cindex.TypeKind.POINTER and
110 t.get_pointee().kind == clang.cindex.TypeKind.CHAR_S and
111 not t.get_pointee().is_const_qualified())
112
113 def IsIntegerType(t):
114 return (t.kind == clang.cindex.TypeKind.INT or
115 t.spelling in [ 'int8_t', 'int16_t', 'int32_t', 'int64_t',
116 'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t'])
117
118 def IsFloatType(t):
119 return t.kind == clang.cindex.TypeKind.FLOAT
120
121 def IsEnumerationType(t):
122 return (t.kind == clang.cindex.TypeKind.TYPEDEF and
123 t.spelling in enumerations)
124
125 def IsTargetMemoryBufferType(t):
126 return (t.kind == clang.cindex.TypeKind.POINTER and
127 not t.get_pointee().is_const_qualified() and
128 t.get_pointee().spelling == 'OrthancPluginMemoryBuffer')
129
130 def IsSourceMemoryBufferType(t):
131 return (t.kind == clang.cindex.TypeKind.POINTER and
132 t.get_pointee().kind == clang.cindex.TypeKind.VOID and
133 t.get_pointee().is_const_qualified())
134
135 def IsClassType(t):
136 return (t.kind == clang.cindex.TypeKind.POINTER and
137 ((t.get_pointee().is_const_qualified() and
138 t.get_pointee().spelling.startswith('const ') and
139 t.get_pointee().spelling[len('const '):] in classes) or
140 (not t.get_pointee().is_const_qualified() and
141 t.get_pointee().spelling in classes)))
142
143 def IsSimpleSourceType(t):
144 return (IsSourceStringType(t) or
145 IsFloatType(t) or
146 IsIntegerType(t) or
147 IsEnumerationType(t) or
148 IsSourceMemoryBufferType(t))
149
150 def IsVoidType(t):
151 return t.kind == clang.cindex.TypeKind.VOID
152
153 def IsSupportedTargetType(t):
154 return (IsVoidType(t) or
155 IsIntegerType(t) or
156 IsEnumerationType(t) or
157 # Constructor of a class
158 (t.kind == clang.cindex.TypeKind.POINTER and
159 not t.get_pointee().is_const_qualified() and
160 t.get_pointee().spelling in classes) or
161 # "const char*" or "char*" outputs
162 (t.kind == clang.cindex.TypeKind.POINTER and
163 #not t.get_pointee().is_const_qualified() and
164 t.get_pointee().kind == clang.cindex.TypeKind.CHAR_S))
165
166 def IsBytesArgument(args, index):
167 return (index + 1 < len(args) and
168 args[index].type.kind == clang.cindex.TypeKind.POINTER and
169 args[index].type.get_pointee().kind == clang.cindex.TypeKind.VOID and
170 args[index].type.get_pointee().is_const_qualified() and
171 args[index + 1].type.spelling == 'uint32_t')
172
173 def CheckOnlySupportedArguments(args):
174 j = 0
175 while j < len(args):
176 if IsBytesArgument(args, j):
177 j += 2
178 elif IsSimpleSourceType(args[j].type):
179 j += 1
180 else:
181 return False
182 return True
183
184
185 ORTHANC_TO_PYTHON_NUMERIC_TYPES = {
186 # https://docs.python.org/3/c-api/arg.html#numbers
187 'int' : {
188 'type' : 'int',
189 'format' : 'i',
190 },
191 'uint8_t' : {
192 'type' : 'unsigned char',
193 'format' : 'b',
194 },
195 'int32_t' : {
196 'type' : 'long int',
197 'format' : 'l',
198 },
199 'uint16_t' : {
200 'type' : 'unsigned short',
201 'format' : 'H',
202 },
203 'uint32_t' : {
204 'type' : 'unsigned long',
205 'format' : 'k',
206 },
207 'uint64_t' : {
208 'type' : 'unsigned long long',
209 'format' : 'K',
210 },
211 'float' : {
212 'type' : 'float',
213 'format' : 'f',
214 }
215 }
216
217
218 def GenerateFunctionBodyTemplate(cFunction, result_type, args):
219 if not cFunction.startswith('OrthancPlugin'):
220 raise Exception()
221
222 func = {
223 'c_function' : cFunction,
224 'short_name' : cFunction[len('OrthancPlugin'):],
225 'args' : [],
226 }
227
228 if IsIntegerType(result_type):
229 func['return_long'] = True
230 elif IsTargetDynamicStringType(result_type):
231 func['return_dynamic_string'] = True
232 elif IsTargetStaticStringType(result_type):
233 func['return_static_string'] = True
234 elif IsVoidType(result_type):
235 func['return_void'] = True
236 elif result_type.spelling == 'OrthancPluginErrorCode':
237 func['return_error'] = True
238 elif IsClassType(result_type):
239 func['return_object'] = result_type.get_pointee().spelling
240 elif IsTargetMemoryBufferType(result_type):
241 func['return_bytes'] = True
242 elif IsEnumerationType(result_type):
243 func['return_enumeration'] = result_type.spelling
244 else:
245 raise Exception('Not supported: %s' % result_type.spelling)
246
247 i = 0
248 while i < len(args):
249 a = {
250 'name' : 'arg%d' % i,
251 }
252
253 if (IsIntegerType(args[i].type) or
254 IsFloatType(args[i].type)):
255 t = ORTHANC_TO_PYTHON_NUMERIC_TYPES[args[i].type.spelling]
256 a['python_type'] = t['type']
257 a['python_format'] = t['format']
258 a['initialization'] = ' = 0'
259 a['orthanc_cast'] = 'arg%d' % i
260 func['args'].append(a)
261 elif IsSourceStringType(args[i].type):
262 a['python_type'] = 'const char*'
263 a['python_format'] = 's'
264 a['initialization'] = ' = NULL'
265 a['orthanc_cast'] = 'arg%d' % i
266 func['args'].append(a)
267 elif IsEnumerationType(args[i].type):
268 a['python_type'] = 'long int'
269 a['python_format'] = 'l'
270 a['initialization'] = ' = 0'
271 a['orthanc_cast'] = 'static_cast<%s>(arg%d)' % (args[i].type.spelling, i)
272 func['args'].append(a)
273 elif IsBytesArgument(args, i):
274 a['python_type'] = 'Py_buffer'
275 # In theory, one should use "y*" (this is the recommended
276 # way to accept binary data). However, this is not
277 # available in Python 2.7
278 a['python_format'] = 's*'
279 a['orthanc_cast'] = 'arg%d.buf, arg%d.len' % (i, i)
280 a['release'] = 'PyBuffer_Release(&arg%d);' % i
281 func['args'].append(a)
282 i += 1
283 elif IsSourceMemoryBufferType(args[i].type):
284 a['python_type'] = 'Py_buffer'
285 a['python_format'] = 's*'
286 a['orthanc_cast'] = 'arg%d.buf' % i
287 a['release'] = 'PyBuffer_Release(&arg%d);' % i
288 func['args'].append(a)
289 else:
290 raise Exception('Not supported: %s, %s' % (cFunction, args[i].spelling))
291
292 i += 1
293
294 func['tuple_format'] = '"%s", %s' % (
295 ''.join(map(lambda x: x['python_format'], func['args'])),
296 ', '.join(map(lambda x: '&' + x['name'], func['args'])))
297
298 if len(func['args']) > 0:
299 func['count_args'] = len(func['args'])
300 func['has_args'] = True
301 func['call_args'] = ', ' + ', '.join(map(lambda x: x['orthanc_cast'], func['args']))
302
303 return func
304
305
306 for node in tu.cursor.get_children():
307 if node.kind == clang.cindex.CursorKind.ENUM_DECL:
308 if node.type.spelling.startswith('OrthancPlugin'):
309 name = node.type.spelling
310
311 values = []
312 for item in node.get_children():
313 if (item.kind == clang.cindex.CursorKind.ENUM_CONSTANT_DECL and
314 item.spelling.startswith(name + '_')):
315 values.append({
316 'key' : ToUpperCase(item.spelling[len(name)+1:]),
317 'value' : item.enum_value
318 })
319
320 path = 'sdk_%s.impl.h' % name
321 shortName = name[len('OrthancPlugin'):]
322
323 with open(os.path.join(TARGET, path), 'w') as f:
324 f.write(pystache.render(TEMPLATE, {
325 'name' : name,
326 'short_name' : shortName,
327 'values' : values,
328 }))
329
330 enumerations[name] = {
331 'name' : name,
332 'path' : path,
333 }
334
335 elif node.kind == clang.cindex.CursorKind.FUNCTION_DECL:
336 if node.spelling.startswith('OrthancPlugin'):
337 #if node.spelling != 'OrthancPluginWorklistGetDicomQuery':
338 # continue
339 shortName = node.spelling[len('OrthancPlugin'):]
340
341 # Check that the first argument is the Orthanc context
342 args = list(filter(lambda x: x.kind == clang.cindex.CursorKind.PARM_DECL,
343 node.get_children()))
344
345 if (len(args) == 0 or
346 args[0].type.kind != clang.cindex.TypeKind.POINTER or
347 args[0].type.get_pointee().spelling != 'OrthancPluginContext'):
348 print('Not in the Orthanc SDK: %s()' % node.spelling)
349 continue
350
351 # Discard the context from the arguments
352 args = args[1:]
353
354 if not IsSupportedTargetType(node.result_type):
355 print('*** UNSUPPORTED OUTPUT: %s' % node.spelling)
356
357 elif (len(args) == 1 and
358 IsClassType(args[0].type) and
359 node.spelling.startswith('OrthancPluginFree')):
360 print('Destructor: %s' % node.spelling)
361 className = args[0].type.get_pointee().spelling
362 classes[className]['destructor'] = node.spelling
363
364 elif CheckOnlySupportedArguments(args):
365 if IsClassType(node.result_type):
366 print('Constructor: %s' % node.spelling)
367 else:
368 print('Simple global function: %s => %s' % (node.spelling, node.result_type.spelling))
369
370 body = GenerateFunctionBodyTemplate(node.spelling, node.result_type, args)
371 globalFunctions.append(body)
372
373 elif (len(args) >= 2 and
374 IsTargetMemoryBufferType(args[0].type) and
375 CheckOnlySupportedArguments(args[1:])):
376 print('Simple global function, returning bytes: %s' % node.spelling)
377
378 body = GenerateFunctionBodyTemplate(node.spelling, args[0].type, args[1:])
379 globalFunctions.append(body)
380
381 elif (IsClassType(args[0].type) and
382 CheckOnlySupportedArguments(args[1:])):
383 className = args[0].type.get_pointee().spelling
384
385 if className.startswith('const '):
386 className = className[len('const '):]
387
388 print('Simple method of class %s: %s' % (className, node.spelling))
389
390 method = GenerateFunctionBodyTemplate(node.spelling, node.result_type, args[1:])
391 method['self'] = ', self->object_'
392 classes[className]['methods'].append(method)
393
394 elif (len(args) >= 2 and
395 IsTargetMemoryBufferType(args[0].type) and
396 IsClassType(args[1].type) and
397 CheckOnlySupportedArguments(args[2:])):
398 print('Simple method of class %s, returning bytes: %s' % (
399 args[0].type.get_pointee().spelling,
400 node.spelling))
401
402 method = GenerateFunctionBodyTemplate(node.spelling, args[0].type, args[2:])
403 method['self'] = ', self->object_'
404 classes[className]['methods'].append(method)
405
406 else:
407 print('*** UNSUPPORTED INPUT: %s' % node.spelling)
408
409
410
411 elif node.kind == clang.cindex.CursorKind.STRUCT_DECL:
412 if (node.spelling.startswith('_OrthancPlugin') and
413 node.spelling.endswith('_t') and
414 node.spelling != '_OrthancPluginContext_t'):
415 name = node.spelling[len('_') : -len('_t')]
416 classes[name] = {
417 'class_name' : name,
418 'short_name' : name[len('OrthancPlugin'):],
419 'methods' : [ ]
420 }
421
422
423
424
425 partials = {}
426
427 with open(os.path.join(ROOT, 'FunctionBody.mustache'), 'r') as f:
428 partials['function_body'] = f.read()
429
430 renderer = pystache.Renderer(
431 escape = lambda u: u, # No escaping
432 partials = partials,
433 )
434
435 with open(os.path.join(ROOT, 'Class.mustache'), 'r') as f:
436 template = f.read()
437
438 for (key, value) in classes.items():
439 with open(os.path.join(TARGET, 'sdk_%s.impl.h' % value['class_name']), 'w') as h:
440 h.write(renderer.render(template, value))
441
442
443 def FlattenDictionary(source):
444 result = []
445 for (key, value) in source.items():
446 result.append(value)
447 return result
448
449
450 with open(os.path.join(ROOT, 'GlobalFunctions.mustache'), 'r') as f:
451 with open(os.path.join(TARGET, 'sdk_GlobalFunctions.impl.h'), 'w') as h:
452 h.write(renderer.render(f.read(), {
453 'global_functions' : globalFunctions,
454 }))
455
456 with open(os.path.join(ROOT, 'sdk.cpp.mustache'), 'r') as f:
457 with open(os.path.join(TARGET, 'sdk.cpp'), 'w') as h:
458 h.write(renderer.render(f.read(), {
459 'classes' : FlattenDictionary(classes),
460 'enumerations' : FlattenDictionary(enumerations),
461 'global_functions' : globalFunctions,
462 }))
463
464 with open(os.path.join(ROOT, 'sdk.h.mustache'), 'r') as f:
465 with open(os.path.join(TARGET, 'sdk.h'), 'w') as h:
466 h.write(renderer.render(f.read(), {
467 'classes' : FlattenDictionary(classes),
468 }))