Mercurial > hg > orthanc-python
changeset 173:3c72d1f4c2a5 java-code-model
removed dependency on clang
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Thu, 27 Jun 2024 17:50:51 +0200 |
parents | 8382c7dea471 |
children | e9be3c9294d4 |
files | CodeAnalysis/Dockerfile CodeAnalysis/ParseOrthancSDK.py CodeAnalysis/README.txt CodeAnalysis/docker-compose.yml |
diffstat | 4 files changed, 7 insertions(+), 640 deletions(-) [+] |
line wrap: on
line diff
--- a/CodeAnalysis/Dockerfile Thu Jun 27 17:47:05 2024 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,20 +0,0 @@ -FROM ubuntu:18.04 - -RUN export DEBIAN_FRONTEND=noninteractive -RUN apt-get --assume-yes update -RUN apt-get --assume-yes install build-essential -RUN apt-get --assume-yes install python-clang-4.0 -RUN apt-get --assume-yes install python-pip -RUN apt-get --assume-yes install clang-4.0 - -# force pystache 0.5.0 (last version supported for python 2.7) -RUN pip install pystache==0.5.0 - -RUN mkdir /source -RUN mkdir /target -RUN mkdir /CodeAnalysis -COPY *.mustache /CodeAnalysis/ -COPY *.py /CodeAnalysis/ - -ENTRYPOINT ["python2"] -CMD ["./CodeAnalysis/ParseOrthancSDK.py", "--libclang=libclang-4.0.so.1", "--source", "source/OrthancCPlugin.h", "--target", "target/"]
--- a/CodeAnalysis/ParseOrthancSDK.py Thu Jun 27 17:47:05 2024 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,577 +0,0 @@ -#!/usr/bin/env python3 - -## -## Python plugin for Orthanc -## Copyright (C) 2020-2023 Osimis S.A., Belgium -## Copyright (C) 2024-2024 Orthanc Team SRL, Belgium -## Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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/>. -## - - -import argparse -import clang.cindex -import json -import os -import pprint -import pystache -import sys - - -ROOT = os.path.dirname(os.path.realpath(sys.argv[0])) - - -## -## Configuration of the custom primitives that are manually -## implemented (not autogenerated) -## - -CUSTOM_FUNCTIONS = set([ - 'OrthancPluginCreateDicom', - 'OrthancPluginCreateImageAccessor', # Replaced by "orthanc.CreateImageFromBuffer()" - 'OrthancPluginFreeMemoryBuffer', - 'OrthancPluginFreeString', - 'OrthancPluginLookupDictionary', - 'OrthancPluginRegisterFindCallback', - 'OrthancPluginRegisterIncomingHttpRequestFilter', # Implemented through v2 - 'OrthancPluginRegisterIncomingHttpRequestFilter2', - 'OrthancPluginRegisterMoveCallback', - 'OrthancPluginRegisterOnChangeCallback', - 'OrthancPluginRegisterOnStoredInstanceCallback', - 'OrthancPluginRegisterRestCallback', # Implemented using OrthancPlugins::RegisterRestCallback - 'OrthancPluginRegisterRestCallbackNoLock', # Implemented using OrthancPlugins::RegisterRestCallback - 'OrthancPluginRegisterWorklistCallback', - 'OrthancPluginRegisterIncomingCStoreInstanceFilter', -]) - -CUSTOM_METHODS = [ - { - 'class_name' : 'OrthancPluginFindQuery', - 'method_name' : 'GetFindQueryTagGroup', - 'implementation' : 'GetFindQueryTagGroup', - 'sdk_function' : 'OrthancPluginGetFindQueryTag', - }, - { - 'class_name' : 'OrthancPluginFindQuery', - 'method_name' : 'GetFindQueryTagElement', - 'implementation' : 'GetFindQueryTagElement', - 'sdk_function' : 'OrthancPluginGetFindQueryTag', - }, - { - 'class_name' : 'OrthancPluginWorklistAnswers', - 'method_name' : 'WorklistAddAnswer', - 'implementation' : 'WorklistAddAnswer', - 'sdk_function' : 'OrthancPluginWorklistAddAnswer', - }, - { - 'class_name' : 'OrthancPluginDicomInstance', - 'method_name' : 'GetInstanceData', - 'implementation' : 'GetInstanceData', - 'sdk_function' : 'OrthancPluginGetInstanceData', - }, - { - 'class_name' : 'OrthancPluginImage', - 'method_name' : 'GetImageBuffer', - 'implementation' : 'GetImageBuffer', - 'sdk_function' : 'OrthancPluginGetImageBuffer', - }, -] - -for method in CUSTOM_METHODS: - CUSTOM_FUNCTIONS.add(method['sdk_function']) - - -## -## 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.10.0/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 = [] -countAllFunctions = 0 -countSupportedFunctions = 0 - -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' : [], - 'return_sdk_type' : result_type.spelling, - } - - 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 - func['return_sdk_type'] = 'enumeration' - func['return_sdk_enumeration'] = result_type.spelling - elif IsClassType(result_type): - func['return_object'] = result_type.get_pointee().spelling - func['return_sdk_type'] = 'object' - func['return_sdk_class'] = result_type.get_pointee().spelling - elif IsTargetMemoryBufferType(result_type): - func['return_bytes'] = True - elif IsEnumerationType(result_type): - func['return_enumeration'] = result_type.spelling - func['return_sdk_type'] = 'enumeration' - func['return_sdk_enumeration'] = result_type.spelling - else: - raise Exception('Not supported: %s' % result_type.spelling) - - i = 0 - while i < len(args): - a = { - 'name' : 'arg%d' % i, - 'sdk_type' : args[i].type.spelling, - 'sdk_name' : args[i].spelling, - } - - 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) - a['sdk_type'] = 'enumeration' - a['sdk_enumeration'] = args[i].type.spelling - 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 - a['sdk_type'] = 'const_void_pointer_with_size' - func['args'].append(a) - i += 1 # Skip the size argument - 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, - 'values' : values, - } - - 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 - countAllFunctions += 1 - args = args[1:] - - if node.spelling in CUSTOM_FUNCTIONS: - print('Ignoring custom function that is manually implemented: %s()' % node.spelling) - countSupportedFunctions += 1 - - elif 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 - countSupportedFunctions += 1 - - 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) - countSupportedFunctions += 1 - - 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) - countSupportedFunctions += 1 - - elif (IsClassType(args[0].type) and - CheckOnlySupportedArguments(args[1:])): - className = args[0].type.get_pointee().spelling - - print('Simple method of class %s: %s' % (className, node.spelling)) - if node.spelling in CUSTOM_FUNCTIONS: - raise Exception('Cannot overwrite an autogenerated method: %s()' % node.spelling) - - if className.startswith('const '): - className = className[len('const '):] - - method = GenerateFunctionBodyTemplate(node.spelling, node.result_type, args[1:]) - method['self'] = ', self->object_' - classes[className]['methods'].append(method) - countSupportedFunctions += 1 - - elif (len(args) >= 2 and - IsTargetMemoryBufferType(args[0].type) and - IsClassType(args[1].type) and - CheckOnlySupportedArguments(args[2:])): - className = args[1].type.get_pointee().spelling - - print('Simple method of class %s, returning bytes: %s' % (className, node.spelling)) - if node.spelling in CUSTOM_FUNCTIONS: - raise Exception('Cannot overwrite an autogenerated method: %s()' % node.spelling) - - if className.startswith('const '): - className = className[len('const '):] - - method = GenerateFunctionBodyTemplate(node.spelling, args[0].type, args[2:]) - method['self'] = ', self->object_' - classes[className]['methods'].append(method) - countSupportedFunctions += 1 - - 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' : [ ], - 'custom_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: - with open(os.path.join(ROOT, 'ClassMethods.mustache'), 'r') as g: - classDefinition = f.read() - classMethods = g.read() - - for method in CUSTOM_METHODS: - classes[method['class_name']]['custom_methods'].append(method) - - 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(classDefinition, value)) - with open(os.path.join(TARGET, 'sdk_%s.methods.h' % value['class_name']), 'w') as h: - h.write(renderer.render(classMethods, value)) - - -def FlattenDictionary(source): - result = [] - for (key, value) in source.items(): - result.append(value) - return result - - -sortedClasses = sorted(FlattenDictionary(classes), key = lambda x: x['class_name']) -sortedEnumerations = sorted(FlattenDictionary(enumerations), key = lambda x: x['name']) -sortedGlobalFunctions = sorted(globalFunctions, key = lambda x: x['c_function']) - -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' : sortedGlobalFunctions, - })) - -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' : sortedClasses, - 'enumerations' : sortedEnumerations, - 'global_functions' : sortedGlobalFunctions, - })) - -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' : sortedClasses, - })) - - -print('') -print('Total functions in the SDK: %d' % countAllFunctions) -print('Total supported functions: %d' % countSupportedFunctions) -print('Coverage: %.0f%%' % (float(countSupportedFunctions) / - float(countAllFunctions) * 100.0))
--- a/CodeAnalysis/README.txt Thu Jun 27 17:47:05 2024 +0200 +++ b/CodeAnalysis/README.txt Thu Jun 27 17:50:51 2024 +0200 @@ -2,40 +2,14 @@ Introduction ============ -This folder contains the Python script that analyzes the header of the -Orthanc Plugin SDK using clang, then automatically wraps this SDK as a -Python extension module. The output of the script is written to -"../Sources/Autogenerated". - - -Usage on Ubuntu 18.04 -===================== +This folder contains the Python script that reads the code model of +the Orthanc SDK that is generated by the "orthanc-java" project [1], +then automatically wraps this SDK as a Python extension module. -$ sudo apt-get install python-clang-4.0 python-pystache -$ python2 ./ParseOrthancSDK.py --libclang=libclang-4.0.so.1 \ - --source ../Resources/Orthanc/Sdk-1.10.0/orthanc/OrthancCPlugin.h \ - --target ../Sources/Autogenerated - - -Usage on Ubuntu 20.04 -===================== +The output of the script is written to "../Sources/Autogenerated". -$ sudo apt-get install python-clang-6.0 python3-pystache -$ python3 ./ParseOrthancSDK.py --libclang=libclang-6.0.so.1 \ - --source ../Resources/Orthanc/Sdk-1.10.0/orthanc/OrthancCPlugin.h \ - --target ../Sources/Autogenerated - - -Usage on Ubuntu 22.04 -===================== +Usage: -$ sudo apt-get install python3-clang-14 python3-pystache -$ python3 ./ParseOrthancSDK.py --libclang=libclang-14.so.1 \ - --source ../Resources/Orthanc/Sdk-1.10.0/orthanc/OrthancCPlugin.h \ - --target ../Sources/Autogenerated +$ ./GenerateOrthancSDK.py - -Run through docker -================== - -$ docker-compose up --build +[1] https://orthanc.uclouvain.be/book/plugins/java.html
--- a/CodeAnalysis/docker-compose.yml Thu Jun 27 17:47:05 2024 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,10 +0,0 @@ -version: "3" - -services: - orthanc-sdk-parser: - build: . - volumes: - - ../Sources/Autogenerated:/target - - ../Resources/Orthanc/Sdk-1.10.0/orthanc/:/source - # to parse your local mainline orthanc, use the below line instead - # - /home/alain/o/orthanc/OrthancServer/Plugins/Include/orthanc/:/source