Mercurial > hg > orthanc-java
changeset 58:34fa909facd3
moving code model generation to the orthanc core project
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Mon, 11 Aug 2025 13:44:49 +0200 |
parents | c969b004d8c0 |
children | 1873eecbc592 |
files | CodeGeneration/ParseOrthancSDK.py CodeGeneration/README.txt NEWS |
diffstat | 3 files changed, 3 insertions(+), 616 deletions(-) [+] |
line wrap: on
line diff
--- a/CodeGeneration/ParseOrthancSDK.py Sun Aug 10 13:20:30 2025 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,605 +0,0 @@ -#!/usr/bin/env python3 - -# SPDX-FileCopyrightText: 2023-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium -# SPDX-License-Identifier: GPL-3.0-or-later - -# Java plugin for Orthanc -# Copyright (C) 2023-2025 Sebastien Jodogne, ICTEAM 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])) - - -with open(os.path.join(os.path.dirname(__file__), '..', 'OrthancSDKVersion.cmake'), 'r') as f: - m = re.match('^set\(ORTHANC_SDK_VERSION "([0-9.]+)"\)$', f.read(), re.MULTILINE) - assert(m != None) - PLUGIN_SDK_VERSION = m.group(1) - - -parser = argparse.ArgumentParser(description = 'Parse the Orthanc SDK.') -parser.add_argument('--libclang', - default = 'libclang-14.so.1', - help = 'manually provides the path to the libclang shared library') -parser.add_argument('--source', - default = os.path.join(ROOT, '../Resources/Orthanc/Sdk-%s/orthanc/OrthancCPlugin.h' % PLUGIN_SDK_VERSION), - help = 'input path to the Orthanc SDK header') -parser.add_argument('--target', - default = os.path.join(ROOT, '../Resources/CodeModel-%s.json' % PLUGIN_SDK_VERSION), - 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 ', '') - -def InjectSinceSdk(target, node): - since_sdk = None - - for child in node.get_children(): - if child.kind == clang.cindex.CursorKind.ANNOTATE_ATTR: - s = child.spelling.split(' ') - if s[0] == 'ORTHANC_PLUGIN_SINCE_SDK': - assert(len(s) == 2) - version = s[1].split('.') - assert(len(version) == 3) - assert(since_sdk == None) # Cannot be defined multiple time - since_sdk = [ int(version[0]), int(version[1]), int(version[2]) ] - - if since_sdk != None: - target['since_sdk'] = since_sdk - - -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 + '_'):] - value = { - 'key' : key, - 'value' : item.enum_value, - 'documentation' : ParseEnumValueDocumentation(item.raw_comment), - } - - InjectSinceSdk(value, item) - values.append(value) - - elif (item.kind == clang.cindex.CursorKind.ENUM_CONSTANT_DECL and - item.spelling == '_%s_INTERNAL' % name): - pass - - elif (item.kind == clang.cindex.CursorKind.ANNOTATE_ATTR and - item.spelling.startswith('ORTHANC_PLUGIN_SINCE_SDK ')): - pass - - else: - raise Exception('Ignoring unknown enumeration item: %s' % item.spelling) - - value = { - 'values' : values, - 'documentation' : ParseEnumerationDocumentation(node.raw_comment), - } - - InjectSinceSdk(value, node) - enumerations[name] = value - - 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')] - value = { - 'name' : name, - 'methods' : [ ], - } - - InjectSinceSdk(value, node) - classes[name] = value - - 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')): - - if node.spelling in SPECIAL_FUNCTIONS: - countAllFunctions += 1 - continue - - 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, - } - - InjectSinceSdk(method, node) - - 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, - } - - InjectSinceSdk(f, node) - - 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(): - item = { - 'name' : name, - 'values' : content['values'], - 'documentation' : content['documentation'], - } - - if 'since_sdk' in content: - item['since_sdk'] = content['since_sdk'] - - result.append(item) - - 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 (including destructors): %d' % countWrappedFunctions) -print('Coverage: %.0f%%' % (float(countWrappedFunctions) / - float(countAllFunctions) * 100.0))
--- a/CodeGeneration/README.txt Sun Aug 10 13:20:30 2025 +0200 +++ b/CodeGeneration/README.txt Mon Aug 11 13:44:49 2025 +0200 @@ -2,25 +2,16 @@ Introduction ============ -This folder contains the two Python scripts that analyze the header of -the Orthanc Plugin SDK using clang, then extract the code model of the -SDK as a JSON file, and finally create the Java wrapper. - -The code model is written to: ../CodeGeneration/CodeModel.json +This folder contains the Python script that generates the Java wrapper +from the code model of the Orthanc SDK. The Java wrapper is written to: ../JavaSDK/be/uclouvain/orthanc/ The C++ interface is written to: ../Plugin/NativeSDK.cpp -Note that the generated code model is also used in the orthanc-python -project, starting with its release 4.3. - Usage on Ubuntu 22.04 ===================== $ 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 ../Resources/CodeModel-1.10.0.json $ python3 ./CodeGeneration.py --source ../Resources/CodeModel-1.10.0.json
--- a/NEWS Sun Aug 10 13:20:30 2025 +0200 +++ b/NEWS Mon Aug 11 13:44:49 2025 +0200 @@ -1,6 +1,7 @@ Pending changes in the mainline =============================== +* The code model generation was moved to the core Orthanc project. * Added the "since_sdk" information for functions in the code model, as derived from the ORTHANC_PLUGIN_SINCE_SDK macro in SDK >= 1.12.9