diff 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
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/CodeGeneration/ParseOrthancSDK.py	Wed Oct 18 17:59:44 2023 +0200
@@ -0,0 +1,555 @@
+#!/usr/bin/env python3
+
+# SPDX-FileCopyrightText: 2023 Sebastien Jodogne, UCLouvain, Belgium
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Java plugin for Orthanc
+# Copyright (C) 2023 Sebastien Jodogne, UCLouvain, Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU 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
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+import argparse
+import clang.cindex
+import json
+import os
+import pprint
+import pystache
+import re
+import sys
+
+
+ROOT = os.path.dirname(os.path.realpath(sys.argv[0]))
+
+
+parser = argparse.ArgumentParser(description = 'Parse the Orthanc SDK.')
+parser.add_argument('--libclang',
+                    default = 'libclang-6.0.so.1',
+                    help = 'manually provides the path to the libclang shared library')
+parser.add_argument('--source',
+                    default = os.path.join(ROOT, '../Resources/Orthanc/Sdk-1.10.0/orthanc/OrthancCPlugin.h'),
+                    help = 'input path to the Orthanc SDK header')
+parser.add_argument('--target',
+                    default = os.path.join(ROOT, 'CodeModel.json'),
+                    help = 'target path to store the JSON code model')
+
+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)
+
+
+
+SPECIAL_FUNCTIONS = [
+    'OrthancPluginCreateMemoryBuffer',
+    'OrthancPluginCreateMemoryBuffer64',
+    'OrthancPluginFreeMemoryBuffer',
+    'OrthancPluginFreeMemoryBuffer64',
+    'OrthancPluginFreeString',
+    ]
+
+
+
+# First, discover the classes and enumerations
+classes = {}
+enumerations = {}
+
+def ParseDocumentationLines(comment):
+    s = re.sub('^[ ]*/', '', comment)
+    s = re.sub('/[ ]*$', '', s)
+    s = re.sub('<tt>', '"', s)
+    s = re.sub('</tt>', '"', s)
+    return list(map(lambda x: re.sub('[ ]*\*+', '', x).strip(), s.splitlines()))
+
+def ParseEnumerationDocumentation(comment):
+    result = ''
+    for line in ParseDocumentationLines(comment):
+        if len(line) > 0 and not line.startswith('@'):
+            if len(result) == 0:
+                result = line
+            else:
+                result = result + ' ' + line
+    return result
+
+def ParseEnumValueDocumentation(comment):
+    m = re.match(r'/\*!<\s*(.*?)\s*\*/$', comment, re.MULTILINE)
+    if m != None:
+        return m.group(1)
+    else:
+        result = ''
+        for line in ParseDocumentationLines(comment):
+            if len(line) > 0:
+                if len(result) == 0:
+                    result = line
+                else:
+                    result = result + ' ' + line
+        return result.replace('@brief ', '')
+
+for node in tu.cursor.get_children():
+    # Only consider the Orthanc SDK
+    path = node.location.file.name
+    if os.path.split(path) [-1] != 'OrthancCPlugin.h':
+        continue
+
+    if node.kind == clang.cindex.CursorKind.ENUM_DECL:
+        if node.type.spelling.startswith('OrthancPlugin'):
+            name = node.type.spelling
+
+            if name in enumerations:
+                raise Exception('Enumeration declared twice: %s' % name)
+
+            if node.raw_comment == None:
+                raise Exception('Enumeration without documentation: %s' % name)
+
+            values = []
+            for item in node.get_children():
+                if (item.kind == clang.cindex.CursorKind.ENUM_CONSTANT_DECL and
+                    item.spelling.startswith(name + '_')):
+
+                    if item.raw_comment == None:
+                        raise Exception('Enumeration value without documentation: %s' % item.spelling)
+
+                    key = item.spelling[len(name + '_'):]
+                    values.append({
+                        'key' : key,
+                        'value' : item.enum_value,
+                        'documentation' : ParseEnumValueDocumentation(item.raw_comment),
+                    })
+
+                elif (item.kind == clang.cindex.CursorKind.ENUM_CONSTANT_DECL and
+                      item.spelling == '_%s_INTERNAL' % name):
+                    pass
+
+                else:
+                    raise Exception('Ignoring unknown enumeration item: %s' % item.spelling)
+
+            enumerations[name] = {
+                'values' : values,
+                'documentation' : ParseEnumerationDocumentation(node.raw_comment),
+            }
+
+        elif node.spelling == '':  # Unnamed enumeration (presumbaly "_OrthancPluginService")
+            pass
+
+        else:
+            raise Exception('Ignoring unknown enumeration: %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] = {
+                'name' : name,
+                'methods' : [ ],
+            }
+
+        elif node.spelling in [ '',  # This is an internal structure to call Orthanc SDK
+                                '_OrthancPluginContext_t' ]:
+            pass
+
+        else:
+            raise Exception('Ignoring unknown structure: %s' % node.spelling)
+
+
+# Secondly, loop over the C functions and categorize them either as
+# method, or as global functions
+
+
+def RemovePrefix(prefix, s):
+    if not s.startswith(prefix):
+        raise Exception('String "%s" does not start with prefix "%s"' % (s, prefix))
+    else:
+        return s[len(prefix):]
+
+
+def IsClassType(t):
+    return (t.kind == clang.cindex.TypeKind.POINTER and
+            not t.get_pointee().is_const_qualified() and
+            t.get_pointee().spelling in classes)
+
+
+def IsConstClassType(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)
+
+
+def EncodeArguments(target, args):
+    assert(type(target) is dict)
+    result = []
+
+    i = 0
+    while i < len(args):
+        arg = {
+            'name' : 'arg%d' % i,
+            'sdk_name' : args[i].spelling,
+            'sdk_type' : args[i].type.spelling,
+            }
+
+        if (i + 1 < len(args) and
+            args[i].type.spelling == 'const void *' and
+            args[i + 1].type.spelling == 'uint32_t'):
+
+            arg['sdk_type'] = 'const_void_pointer_with_size'
+
+            # Skip the size argument
+            i += 1
+
+        elif arg['sdk_type'] in [ 'float',
+                                  'int32_t',
+                                  'uint8_t',
+                                  'uint16_t',
+                                  'uint32_t',
+                                  'uint64_t',
+                                  'const char *',
+                                  'const void *' ]:
+            pass
+
+        elif arg['sdk_type'] in enumerations:
+            arg['sdk_type'] = 'enumeration'
+            arg['sdk_enumeration'] = args[i].type.spelling
+
+        elif IsClassType(args[i].type):
+            arg['sdk_type'] = 'object'
+            arg['sdk_class'] = args[i].type.get_pointee().spelling
+
+        elif IsConstClassType(args[i].type):
+            arg['sdk_type'] = 'const_object'
+            arg['sdk_class'] = RemovePrefix('const ', args[i].type.get_pointee().spelling)
+
+        else:
+            print('[WARNING] Unsupported argument type in a method (%s), cannot wrap: %s' % (
+                args[i].type.spelling, node.spelling))
+            return False
+
+        result.append(arg)
+        i += 1
+
+    target['args'] = result
+    return True
+
+
+def EncodeResultType(target, returnBufferType, t):
+    assert(type(target) is dict)
+    assert('args' in target)
+
+    target['return_sdk_type'] = t.spelling
+
+    if returnBufferType != None:
+        target['return_sdk_type'] = returnBufferType
+        return True
+
+    elif target['return_sdk_type'] in [ 'void',
+                                        'int32_t',
+                                        'uint32_t',
+                                        'int64_t',
+                                        'char *',
+                                        'const char *' ]:
+        return True
+
+    elif target['return_sdk_type'] in enumerations:
+        target['return_sdk_type'] = 'enumeration'
+        target['return_sdk_enumeration'] = t.spelling
+        return True
+
+    elif IsClassType(t):
+        target['return_sdk_type'] = 'object'
+        target['return_sdk_class'] = t.get_pointee().spelling
+        return True
+
+    else:
+        return False
+
+
+def ParseFunctionDocumentation(comment):
+    lines = ParseDocumentationLines(comment)
+
+    sections = []
+    currentType = None
+    currentSection = None
+
+    for i in range(len(lines)):
+        if lines[i].find('@') > 0:
+            raise Exception('Character "@" not occurring at the beggining of a documentation paragraph')
+
+        if (len(lines[i]) == 0 and
+            currentType == None):
+            continue
+
+        m = re.match(r'^@([a-z]+)\s*', lines[i])
+
+        if m == None:
+            if currentType == None:
+                print(comment)
+                raise Exception('Documentation does not begin with a "@"')
+
+            assert(currentSection != None)
+            currentSection.append(lines[i])
+        else:
+            if currentType != None:
+                sections.append({
+                    'type' : currentType,
+                    'lines' : currentSection,
+                    })
+
+            currentType = m.group(1)
+            currentSection = [ lines[i][m.span() [1] : ] ]
+
+    if currentType == None:
+        raise Exception('Empty documentation')
+
+    sections.append({
+        'type' : currentType,
+        'lines' : currentSection,
+    })
+
+    for i in range(len(sections)):
+        paragraphs = []
+        lines = sections[i]['lines']
+        currentParagraph = ''
+        for j in range(len(lines)):
+            if len(lines[j]) == 0:
+                if currentParagraph != '':
+                    paragraphs.append(currentParagraph)
+                currentParagraph = ''
+            else:
+                if currentParagraph == '':
+                    currentParagraph = lines[j]
+                else:
+                    currentParagraph = '%s %s' % (currentParagraph, lines[j])
+        if currentParagraph != '':
+            paragraphs.append(currentParagraph)
+
+        sections[i]['paragraphs'] = paragraphs
+
+    documentation = {
+        'args' : {}
+    }
+
+    for i in range(len(sections)):
+        t = sections[i]['type']
+        paragraphs = sections[i]['paragraphs']
+
+        if t == 'brief':
+            if len(paragraphs) < 1:
+                raise Exception('Bad @brief')
+
+            documentation['summary'] = paragraphs[0]
+            documentation['description'] = paragraphs[1:]
+
+        elif t in [ 'return', 'result' ]:
+            if len(paragraphs) != 1:
+                raise Exception('Bad @return')
+
+            documentation['return'] = paragraphs[0]
+
+        elif t == 'param':
+            if len(paragraphs) != 1:
+                raise Exception('Bad @param')
+
+            m = re.match(r'^([a-zA-Z0-9]+)\s+(.+)', paragraphs[0])
+            if m == None:
+                raise Exception('Bad @param')
+
+            key = m.group(1)
+            value = m.group(2)
+            if (len(key) == 0 or
+                len(value) == 0):
+                raise Exception('Bad @param')
+
+            if key in documentation['args']:
+                raise Exception('Twice the same parameter: %s' % key)
+
+            documentation['args'][key] = value
+
+        elif t == 'warning':
+            if not 'description' in documentation:
+                raise Exception('@warning before @summary')
+
+            if len(paragraphs) == 0:
+                raise Exception('Bad @warning')
+
+            for j in range(len(paragraphs)):
+                if j == 0:
+                    documentation['description'].append('Warning: %s' % paragraphs[j])
+                else:
+                    documentation['description'].append(paragraphs[j])
+
+        elif t == 'note':
+            if not 'description' in documentation:
+                raise Exception('@note before @summary')
+
+            if len(paragraphs) == 0:
+                raise Exception('Bad @note')
+
+            for j in range(len(paragraphs)):
+                if j == 0:
+                    documentation['description'].append('Remark: %s' % paragraphs[j])
+                else:
+                    documentation['description'].append(paragraphs[j])
+
+        elif t in [
+                'deprecated',
+                'ingroup',
+                'see',
+        ]:
+            pass
+
+        else:
+            raise Exception('Unsupported documentation token: @%s' % t)
+
+    return documentation
+
+
+globalFunctions = []
+countWrappedFunctions = 0
+countAllFunctions = 0
+
+for node in tu.cursor.get_children():
+    # Only consider the Orthanc SDK
+    path = node.location.file.name
+    if os.path.split(path) [-1] != 'OrthancCPlugin.h':
+        continue
+
+    if (node.kind == clang.cindex.CursorKind.FUNCTION_DECL and
+        node.spelling.startswith('OrthancPlugin') and
+        not node.spelling in SPECIAL_FUNCTIONS):
+        args = list(filter(lambda x: x.kind == clang.cindex.CursorKind.PARM_DECL,
+                           node.get_children()))
+
+        # Check that the first argument is the Orthanc context
+        if (len(args) == 0 or
+            args[0].type.kind != clang.cindex.TypeKind.POINTER or
+            args[0].type.get_pointee().spelling != 'OrthancPluginContext'):
+            print('[INFO] Not in the Orthanc SDK: %s()' % node.spelling)
+            continue
+
+        countAllFunctions += 1
+
+        contextName = args[0].spelling
+        args = args[1:]  # Skip the Orthanc context
+
+        if (len(args) >= 1 and
+            args[0].type.spelling in [ 'OrthancPluginMemoryBuffer *',
+                                       'OrthancPluginMemoryBuffer64 *' ]):
+            # The method/function returns a byte array
+            returnBufferType = args[0].type.spelling
+            args = args[1:]
+        else:
+            returnBufferType = None
+
+        if (len(args) >= 1 and
+            (IsClassType(args[0].type) or
+             IsConstClassType(args[0].type))):
+
+            # This is a class method
+            cls = args[0].type.get_pointee().spelling
+            if IsConstClassType(args[0].type):
+                cls = RemovePrefix('const ', cls)
+
+            # Special case of destructors
+            if (len(args) == 1 and
+                not args[0].type.get_pointee().is_const_qualified() and
+                node.spelling.startswith('OrthancPluginFree')):
+                classes[cls]['destructor'] = node.spelling
+                countWrappedFunctions += 1
+
+            else:
+                if node.raw_comment == None:
+                    raise Exception('Method without documentation: %s' % node.spelling)
+
+                doc = ParseFunctionDocumentation(node.raw_comment)
+                del doc['args'][contextName]      # Remove OrthancPluginContext from documentation
+                del doc['args'][args[0].spelling] # Remove self from documentation
+
+                method = {
+                    'c_function' : node.spelling,
+                    'const' : args[0].type.get_pointee().is_const_qualified(),
+                    'documentation' : doc,
+                    }
+
+                if not EncodeArguments(method, args[1:]):
+                    pass
+                elif EncodeResultType(method, returnBufferType, node.result_type):
+                    classes[cls]['methods'].append(method)
+                    countWrappedFunctions += 1
+                else:
+                    print('[WARNING] Unsupported return type in a method (%s), cannot wrap: %s' % (
+                        node.result_type.spelling, node.spelling))
+
+        else:
+            # This is a global function
+            if node.raw_comment == None:
+                raise Exception('Global function without documentation: %s' % node.spelling)
+
+            doc = ParseFunctionDocumentation(node.raw_comment)
+            del doc['args'][contextName]  # Remove OrthancPluginContext from documentation
+
+            f = {
+                'c_function' : node.spelling,
+                'documentation' : doc,
+            }
+
+            if not EncodeArguments(f, args):
+                pass
+            elif EncodeResultType(f, returnBufferType, node.result_type):
+                globalFunctions.append(f)
+                countWrappedFunctions += 1
+            else:
+                print('[WARNING] Unsupported return type in a global function (%s), cannot wrap: %s' % (
+                    node.result_type.spelling, node.spelling))
+
+
+
+# Thirdly, export the code model
+
+def FlattenEnumerations():
+    result = []
+    for (name, content) in enumerations.items():
+        result.append({
+            'name' : name,
+            'values' : content['values'],
+            'documentation' : content['documentation'],
+            })
+    return result
+
+def FlattenDictionary(source):
+    result = []
+    for (name, value) in source.items():
+        result.append(value)
+    return result
+
+codeModel = {
+    'classes' : sorted(FlattenDictionary(classes), key = lambda x: x['name']),
+    'enumerations' : sorted(FlattenEnumerations(), key = lambda x: x['name']),
+    'global_functions' : globalFunctions,  # Global functions are ordered in the same order as in the C header
+    }
+
+
+with open(TARGET, 'w') as f:
+    f.write(json.dumps(codeModel, sort_keys = True, indent = 4))
+
+print('\nTotal functions in the SDK: %d' % countAllFunctions)
+print('Total wrapped functions: %d' % countWrappedFunctions)
+print('Coverage: %.0f%%' % (float(countWrappedFunctions) /
+                            float(countAllFunctions) * 100.0))