diff 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
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/CodeAnalysis/ParseOrthancSDK.py	Thu Mar 26 18:47:01 2020 +0100
@@ -0,0 +1,468 @@
+#!/usr/bin/env python
+
+##
+## Python plugin for Orthanc
+## Copyright (C) 2017-2020 Osimis S.A., Belgium
+##
+## This program is free software: you can redistribute it and/or
+## modify it under the terms of the GNU Affero General Public License
+## as published by the Free Software Foundation, either version 3 of
+## the License, or (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful, but
+## WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+## Affero General Public License for more details.
+## 
+## You should have received a copy of the GNU Affero General Public License
+## along with this program. If not, see <http://www.gnu.org/licenses/>.
+##
+
+
+# Ubuntu 18.04:
+# sudo apt-get install python-clang-4.0
+# python2 ./ParseOrthancSDK.py --libclang=libclang-4.0.so.1 ../Resources/Orthanc/Sdk-1.5.7/orthanc/OrthancCPlugin.h ../Sources/Autogenerated
+
+
+import argparse
+import clang.cindex
+import os
+import pprint
+import pystache
+import sys
+
+
+ROOT = os.path.dirname(os.path.realpath(sys.argv[0]))
+
+
+##
+## Parse the command-line arguments
+##
+
+parser = argparse.ArgumentParser(description = 'Parse the Orthanc SDK.')
+parser.add_argument('--libclang',
+                    default = 'libclang-4.0.so.1',
+                    help = 'manually provides the path to the libclang shared library')
+parser.add_argument('--source',
+                    default = os.path.join(os.path.dirname(__file__),
+                                           '../Resources/Orthanc/Sdk-1.5.7/orthanc/OrthancCPlugin.h'),
+                    help = 'Input C++ file')
+parser.add_argument('--target', 
+                    default = os.path.join(os.path.dirname(__file__),
+                                           '../Sources/Autogenerated'),
+                    help = 'Target folder')
+
+args = parser.parse_args()
+
+
+
+if len(args.libclang) != 0:
+    clang.cindex.Config.set_library_file(args.libclang)
+
+index = clang.cindex.Index.create()
+
+tu = index.parse(args.source, [ ])
+
+TARGET = os.path.realpath(args.target)
+
+
+
+def ToUpperCase(name):
+    s = ''
+    for i in range(len(name)):
+        if name[i].isupper():
+            if len(s) == 0:
+                s += name[i]
+            elif name[i - 1].islower():
+                s += '_' + name[i]
+            elif (i + 1 < len(name) and
+                  name[i - 1].islower() and
+                  name[i + 1].isupper()):
+                s += '_' + name[i]
+            else:
+                s += name[i]
+        else:
+            s += name[i].upper()
+    return s
+
+
+
+with open(os.path.join(ROOT, 'Enumeration.mustache'), 'r') as f:
+    TEMPLATE = f.read()
+
+
+classes = {}
+enumerations = {}
+globalFunctions = []
+
+def IsSourceStringType(t):
+    return (t.kind == clang.cindex.TypeKind.POINTER and
+            t.get_pointee().kind == clang.cindex.TypeKind.CHAR_S and
+            t.get_pointee().is_const_qualified())
+
+def IsTargetStaticStringType(t):
+    return (t.kind == clang.cindex.TypeKind.POINTER and
+            t.get_pointee().kind == clang.cindex.TypeKind.CHAR_S and
+            t.get_pointee().is_const_qualified())
+
+def IsTargetDynamicStringType(t):
+    return (t.kind == clang.cindex.TypeKind.POINTER and
+            t.get_pointee().kind == clang.cindex.TypeKind.CHAR_S and
+            not t.get_pointee().is_const_qualified())
+
+def IsIntegerType(t):
+    return (t.kind == clang.cindex.TypeKind.INT or
+            t.spelling in [ 'int8_t', 'int16_t', 'int32_t', 'int64_t',
+                            'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t'])
+
+def IsFloatType(t):
+    return t.kind == clang.cindex.TypeKind.FLOAT
+
+def IsEnumerationType(t):
+    return (t.kind == clang.cindex.TypeKind.TYPEDEF and
+            t.spelling in enumerations)
+
+def IsTargetMemoryBufferType(t):
+    return (t.kind == clang.cindex.TypeKind.POINTER and
+            not t.get_pointee().is_const_qualified() and
+            t.get_pointee().spelling == 'OrthancPluginMemoryBuffer')    
+
+def IsSourceMemoryBufferType(t):
+    return (t.kind == clang.cindex.TypeKind.POINTER and
+            t.get_pointee().kind == clang.cindex.TypeKind.VOID and
+            t.get_pointee().is_const_qualified())
+
+def IsClassType(t):
+    return (t.kind == clang.cindex.TypeKind.POINTER and
+            ((t.get_pointee().is_const_qualified() and
+              t.get_pointee().spelling.startswith('const ') and
+              t.get_pointee().spelling[len('const '):] in classes) or
+             (not t.get_pointee().is_const_qualified() and
+              t.get_pointee().spelling in classes)))
+
+def IsSimpleSourceType(t):
+    return (IsSourceStringType(t) or
+            IsFloatType(t) or
+            IsIntegerType(t) or
+            IsEnumerationType(t) or
+            IsSourceMemoryBufferType(t))
+
+def IsVoidType(t):
+    return t.kind == clang.cindex.TypeKind.VOID
+
+def IsSupportedTargetType(t):
+    return (IsVoidType(t) or
+            IsIntegerType(t) or
+            IsEnumerationType(t) or
+            # Constructor of a class
+            (t.kind == clang.cindex.TypeKind.POINTER and
+             not t.get_pointee().is_const_qualified() and
+             t.get_pointee().spelling in classes) or
+            # "const char*" or "char*" outputs
+            (t.kind == clang.cindex.TypeKind.POINTER and
+             #not t.get_pointee().is_const_qualified() and
+             t.get_pointee().kind == clang.cindex.TypeKind.CHAR_S))
+
+def IsBytesArgument(args, index):
+    return (index + 1 < len(args) and
+            args[index].type.kind == clang.cindex.TypeKind.POINTER and
+            args[index].type.get_pointee().kind == clang.cindex.TypeKind.VOID and
+            args[index].type.get_pointee().is_const_qualified() and
+            args[index + 1].type.spelling == 'uint32_t')
+
+def CheckOnlySupportedArguments(args):
+    j = 0
+    while j < len(args):
+        if IsBytesArgument(args, j):
+            j += 2
+        elif IsSimpleSourceType(args[j].type):
+            j += 1
+        else:
+            return False
+    return True
+
+
+ORTHANC_TO_PYTHON_NUMERIC_TYPES = {
+    # https://docs.python.org/3/c-api/arg.html#numbers
+    'int' : {
+        'type' : 'int',
+        'format' : 'i',
+        },
+    'uint8_t' : {
+        'type' : 'unsigned char',
+        'format' : 'b',
+        },
+    'int32_t' : {
+        'type' : 'long int',
+        'format' : 'l',
+        },
+    'uint16_t' : {
+        'type' : 'unsigned short',
+        'format' : 'H',
+        },
+    'uint32_t' : {
+        'type' : 'unsigned long',
+        'format' : 'k',
+        },
+    'uint64_t' : {
+        'type' : 'unsigned long long',
+        'format' : 'K',
+        },
+    'float' : {
+        'type' : 'float',
+        'format' : 'f',
+        }
+    }
+
+
+def GenerateFunctionBodyTemplate(cFunction, result_type, args):
+    if not cFunction.startswith('OrthancPlugin'):
+        raise Exception()
+    
+    func = {
+        'c_function' : cFunction,
+        'short_name' : cFunction[len('OrthancPlugin'):],
+        'args' : [],
+    }
+    
+    if IsIntegerType(result_type):
+        func['return_long'] = True
+    elif IsTargetDynamicStringType(result_type):
+        func['return_dynamic_string'] = True
+    elif IsTargetStaticStringType(result_type):
+        func['return_static_string'] = True
+    elif IsVoidType(result_type):
+        func['return_void'] = True
+    elif result_type.spelling == 'OrthancPluginErrorCode':
+        func['return_error'] = True
+    elif IsClassType(result_type):
+        func['return_object'] = result_type.get_pointee().spelling
+    elif IsTargetMemoryBufferType(result_type):
+        func['return_bytes'] = True
+    elif IsEnumerationType(result_type):
+        func['return_enumeration'] = result_type.spelling
+    else:
+        raise Exception('Not supported: %s' % result_type.spelling)
+
+    i = 0
+    while i < len(args):
+        a = {
+            'name' : 'arg%d' % i,
+            }
+
+        if (IsIntegerType(args[i].type) or
+            IsFloatType(args[i].type)):
+            t = ORTHANC_TO_PYTHON_NUMERIC_TYPES[args[i].type.spelling]
+            a['python_type'] = t['type']
+            a['python_format'] = t['format']
+            a['initialization'] = ' = 0'
+            a['orthanc_cast'] = 'arg%d' % i
+            func['args'].append(a)
+        elif IsSourceStringType(args[i].type):
+            a['python_type'] = 'const char*'
+            a['python_format'] = 's'
+            a['initialization'] = ' = NULL'
+            a['orthanc_cast'] = 'arg%d' % i
+            func['args'].append(a)
+        elif IsEnumerationType(args[i].type):
+            a['python_type'] = 'long int'
+            a['python_format'] = 'l'
+            a['initialization'] = ' = 0'
+            a['orthanc_cast'] = 'static_cast<%s>(arg%d)' % (args[i].type.spelling, i)
+            func['args'].append(a)
+        elif IsBytesArgument(args, i):
+            a['python_type'] = 'Py_buffer'
+            # In theory, one should use "y*" (this is the recommended
+            # way to accept binary data). However, this is not
+            # available in Python 2.7
+            a['python_format'] = 's*'
+            a['orthanc_cast'] = 'arg%d.buf, arg%d.len' % (i, i)
+            a['release'] = 'PyBuffer_Release(&arg%d);' % i
+            func['args'].append(a)
+            i += 1
+        elif IsSourceMemoryBufferType(args[i].type):
+            a['python_type'] = 'Py_buffer'
+            a['python_format'] = 's*'
+            a['orthanc_cast'] = 'arg%d.buf' % i
+            a['release'] = 'PyBuffer_Release(&arg%d);' % i
+            func['args'].append(a)
+        else:
+            raise Exception('Not supported: %s, %s' % (cFunction, args[i].spelling))
+
+        i += 1
+        
+    func['tuple_format'] = '"%s", %s' % (
+        ''.join(map(lambda x: x['python_format'], func['args'])),
+        ', '.join(map(lambda x: '&' + x['name'], func['args'])))
+
+    if len(func['args']) > 0:
+        func['count_args'] = len(func['args'])
+        func['has_args'] = True
+        func['call_args'] = ', ' + ', '.join(map(lambda x: x['orthanc_cast'], func['args']))
+
+    return func
+             
+
+for node in tu.cursor.get_children():
+    if node.kind == clang.cindex.CursorKind.ENUM_DECL:
+        if node.type.spelling.startswith('OrthancPlugin'):
+            name = node.type.spelling
+
+            values = []
+            for item in node.get_children():
+                if (item.kind == clang.cindex.CursorKind.ENUM_CONSTANT_DECL and
+                    item.spelling.startswith(name + '_')):
+                    values.append({
+                        'key' : ToUpperCase(item.spelling[len(name)+1:]),
+                        'value' : item.enum_value
+                    })
+
+            path = 'sdk_%s.impl.h' % name
+            shortName = name[len('OrthancPlugin'):]
+
+            with open(os.path.join(TARGET, path), 'w') as f:
+                f.write(pystache.render(TEMPLATE, {
+                    'name' : name,
+                    'short_name' : shortName,
+                    'values' : values,
+                }))
+
+            enumerations[name] = {
+                'name' : name,
+                'path' : path,
+            }
+
+    elif node.kind == clang.cindex.CursorKind.FUNCTION_DECL:
+        if node.spelling.startswith('OrthancPlugin'):
+            #if node.spelling != 'OrthancPluginWorklistGetDicomQuery':
+            #    continue
+            shortName = node.spelling[len('OrthancPlugin'):]
+            
+            # Check that the first argument is the Orthanc context
+            args = list(filter(lambda x: x.kind == clang.cindex.CursorKind.PARM_DECL,
+                               node.get_children()))
+
+            if (len(args) == 0 or
+                args[0].type.kind != clang.cindex.TypeKind.POINTER or
+                args[0].type.get_pointee().spelling != 'OrthancPluginContext'):
+                print('Not in the Orthanc SDK: %s()' % node.spelling)
+                continue
+
+            # Discard the context from the arguments
+            args = args[1:]
+
+            if not IsSupportedTargetType(node.result_type):
+                print('*** UNSUPPORTED OUTPUT: %s' % node.spelling)
+            
+            elif (len(args) == 1 and
+                  IsClassType(args[0].type) and
+                  node.spelling.startswith('OrthancPluginFree')):
+                print('Destructor: %s' % node.spelling)
+                className = args[0].type.get_pointee().spelling
+                classes[className]['destructor'] = node.spelling
+
+            elif CheckOnlySupportedArguments(args):
+                if IsClassType(node.result_type):
+                    print('Constructor: %s' % node.spelling)
+                else:
+                    print('Simple global function: %s => %s' % (node.spelling, node.result_type.spelling))
+
+                body = GenerateFunctionBodyTemplate(node.spelling, node.result_type, args)
+                globalFunctions.append(body)
+
+            elif (len(args) >= 2 and
+                  IsTargetMemoryBufferType(args[0].type) and
+                  CheckOnlySupportedArguments(args[1:])):
+                print('Simple global function, returning bytes: %s' % node.spelling)
+
+                body = GenerateFunctionBodyTemplate(node.spelling, args[0].type, args[1:])
+                globalFunctions.append(body)
+                
+            elif (IsClassType(args[0].type) and
+                  CheckOnlySupportedArguments(args[1:])):
+                className = args[0].type.get_pointee().spelling
+
+                if className.startswith('const '):
+                    className = className[len('const '):]
+                
+                print('Simple method of class %s: %s' % (className, node.spelling))
+
+                method = GenerateFunctionBodyTemplate(node.spelling, node.result_type, args[1:])
+                method['self'] = ', self->object_'
+                classes[className]['methods'].append(method)
+
+            elif (len(args) >= 2 and
+                  IsTargetMemoryBufferType(args[0].type) and
+                  IsClassType(args[1].type) and
+                  CheckOnlySupportedArguments(args[2:])):
+                print('Simple method of class %s, returning bytes: %s' % (
+                    args[0].type.get_pointee().spelling,
+                    node.spelling))
+               
+                method = GenerateFunctionBodyTemplate(node.spelling, args[0].type, args[2:])
+                method['self'] = ', self->object_'
+                classes[className]['methods'].append(method)
+
+            else:
+                print('*** UNSUPPORTED INPUT: %s' % node.spelling)
+            
+
+
+    elif node.kind == clang.cindex.CursorKind.STRUCT_DECL:
+        if (node.spelling.startswith('_OrthancPlugin') and
+            node.spelling.endswith('_t') and
+            node.spelling != '_OrthancPluginContext_t'):
+            name = node.spelling[len('_') : -len('_t')]
+            classes[name] = {
+                'class_name' : name,
+                'short_name' : name[len('OrthancPlugin'):],
+                'methods' : [ ]
+            }
+                    
+
+
+
+partials = {}
+
+with open(os.path.join(ROOT, 'FunctionBody.mustache'), 'r') as f:
+    partials['function_body'] = f.read()
+
+renderer = pystache.Renderer(
+    escape = lambda u: u,  # No escaping
+    partials = partials,
+)
+
+with open(os.path.join(ROOT, 'Class.mustache'), 'r') as f:
+    template = f.read()
+
+    for (key, value) in classes.items():
+        with open(os.path.join(TARGET, 'sdk_%s.impl.h' % value['class_name']), 'w') as h:
+            h.write(renderer.render(template, value))
+        
+
+def FlattenDictionary(source):
+    result = []
+    for (key, value) in source.items():
+        result.append(value)
+    return result
+
+            
+with open(os.path.join(ROOT, 'GlobalFunctions.mustache'), 'r') as f:
+    with open(os.path.join(TARGET, 'sdk_GlobalFunctions.impl.h'), 'w') as h:
+        h.write(renderer.render(f.read(), {
+            'global_functions' : globalFunctions,
+        }))
+            
+with open(os.path.join(ROOT, 'sdk.cpp.mustache'), 'r') as f:
+    with open(os.path.join(TARGET, 'sdk.cpp'), 'w') as h:
+        h.write(renderer.render(f.read(), {
+            'classes' : FlattenDictionary(classes),
+            'enumerations' : FlattenDictionary(enumerations),
+            'global_functions' : globalFunctions,
+        }))
+            
+with open(os.path.join(ROOT, 'sdk.h.mustache'), 'r') as f:
+    with open(os.path.join(TARGET, 'sdk.h'), 'w') as h:
+        h.write(renderer.render(f.read(), {
+            'classes' : FlattenDictionary(classes),
+        }))