view CodeAnalysis/GenerateOrthancSDK.py @ 179:f49864df6f1f java-code-model

trying Py_BEGIN_ALLOW_THREADS
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 27 Jun 2024 21:53:03 +0200
parents e9be3c9294d4
children ddf3e987827f
line wrap: on
line source

#!/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 json
import os
import sys
import pystache

ROOT = os.path.dirname(os.path.realpath(sys.argv[0]))


##
## Parse the command-line arguments
##

parser = argparse.ArgumentParser(description = 'Generate Python code to wrap the Orthanc SDK.')
parser.add_argument('--model',
                    default = os.path.join(os.path.dirname(__file__),
                                           '../Resources/Orthanc/Sdk-1.10.0/CodeModel.json'),
                    help = 'Input code model, as generated by the orthanc-java project')
parser.add_argument('--target',
                    default = os.path.join(os.path.dirname(__file__),
                                           '../Sources/Autogenerated'),
                    help = 'Target folder')

args = parser.parse_args()



##
## Configuration of the custom primitives that are manually
## implemented (not autogenerated)
##

CUSTOM_FUNCTIONS = [
    {
        'c_function' : 'OrthancPluginCreateMemoryBuffer',
        'args' : [
            {
                'name' : 'arg0',
                'sdk_type' : 'uint32_t',
            }
        ],
        'return_sdk_type' : 'OrthancPluginMemoryBuffer *',
    }
]


CUSTOM_METHODS = {
    'OrthancPluginFindQuery' : [
        {
            'method_name' : 'GetFindQueryTagGroup',
            'implementation' : 'GetFindQueryTagGroup',
            'sdk_function' : 'OrthancPluginGetFindQueryTag',
        },
        {
            'method_name' : 'GetFindQueryTagElement',
            'implementation' : 'GetFindQueryTagElement',
            'sdk_function' : 'OrthancPluginGetFindQueryTag',
        },
    ],

    'OrthancPluginDicomInstance' : [
        {
            'method_name' : 'GetInstanceData',
            'implementation' : 'GetInstanceData',
            'sdk_function' : 'OrthancPluginGetInstanceData',
        },
    ],

    'OrthancPluginImage' : [
        {
            'method_name' : 'GetImageBuffer',
            'implementation' : 'GetImageBuffer',
            'sdk_function' : 'OrthancPluginGetImageBuffer',
        }
    ],
}



TARGET = os.path.realpath(args.target)


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(args.model, 'r') as f:
    model = json.loads(f.read())


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


def GetShortName(name):
    if not name.startswith('OrthancPlugin'):
        raise Exception()
    else:
        return name[len('OrthancPlugin'):]



ORTHANC_TO_PYTHON_NUMERIC_TYPES = {
    # https://docs.python.org/3/c-api/arg.html#numbers
    # https://en.wikipedia.org/wiki/C_data_types
    '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 FormatFunction(f):
    answer = {
        'c_function' : f['c_function'],
        'short_name' : GetShortName(f['c_function']),
        'has_args' : len(f['args']) > 0,
        'count_args' : len(f['args']),
    }

    allow_threads = True
    tuple_format = ''
    tuple_target = []
    call_args = []
    args = []

    for arg in f['args']:
        # https://docs.python.org/3/c-api/arg.html
        if arg['sdk_type'] in [ 'const void *', 'const_void_pointer_with_size' ]:
            args.append({
                'name' : arg['name'],
                'python_type' : 'Py_buffer',
                'release' : 'PyBuffer_Release(&%s);' % arg['name'],
            })
            tuple_format += 's*'
            allow_threads = False
        elif arg['sdk_type'] == 'const char *':
            args.append({
                'name' : arg['name'],
                'python_type' : 'const char*',
                'initialization' : ' = NULL',
            })
            tuple_format += 's'
            allow_threads = False
        elif arg['sdk_type'] == 'enumeration':
            args.append({
                'name' : arg['name'],
                'python_type' : 'long int',
                'initialization' : ' = 0',
            })
            tuple_format += 'l'
        elif arg['sdk_type'] == 'const_object':
            args.append({
                'name' : arg['name'],
                'python_type' : 'PyObject*',
                'initialization' : ' = NULL',
                'check_object_type' : arg['sdk_class'],
            })
            tuple_format += 'O'
            allow_threads = False
        elif arg['sdk_type'] in ORTHANC_TO_PYTHON_NUMERIC_TYPES:
            t = ORTHANC_TO_PYTHON_NUMERIC_TYPES[arg['sdk_type']]
            args.append({
                'name' : arg['name'],
                'python_type' : t['type'],
                'initialization' : ' = 0',
            })
            tuple_format += t['format']
        else:
            print('Ignoring function with unsupported argument type: %s(), type = %s' % (f['c_function'], arg['sdk_type']))
            return None

        tuple_target.append('&' + arg['name'])

        if arg['sdk_type'] == 'const void *':
            call_args.append(arg['name'] + '.buf')
        elif arg['sdk_type'] == 'const_void_pointer_with_size':
            call_args.append(arg['name'] + '.buf')
            call_args.append(arg['name'] + '.len')
        elif arg['sdk_type'] == 'enumeration':
            call_args.append('static_cast<%s>(%s)' % (arg['sdk_enumeration'], arg['name']))
        elif arg['sdk_type'] == 'const_object':
            call_args.append('%s == Py_None ? NULL : reinterpret_cast<sdk_%s_Object*>(%s)->object_' % (
                arg['name'], arg['sdk_class'], arg['name']))
        else:
            call_args.append(arg['name'])

    answer['args'] = args

    if f['return_sdk_type'] == 'void':
        answer['return_void'] = True
    elif f['return_sdk_type'] in [ 'int32_t', 'uint32_t', 'int64_t' ]:
        answer['return_long'] = True
    elif f['return_sdk_type'] == 'OrthancPluginMemoryBuffer *':
        answer['return_bytes'] = True
    elif f['return_sdk_type'] == 'enumeration':
        if f['return_sdk_enumeration'] == 'OrthancPluginErrorCode':
            answer['return_error'] = True
        else:
            answer['return_enumeration'] = f['return_sdk_enumeration']
    elif f['return_sdk_type'] == 'char *':
        answer['return_dynamic_string'] = True
    elif f['return_sdk_type'] == 'const char *':
        answer['return_static_string'] = True
    elif f['return_sdk_type'] == 'object':
        answer['return_object'] = f['return_sdk_class']
    else:
        print('Ignoring function with unsupported return type: %s(), type = %s' % (f['c_function'], f['return_sdk_type']))
        return None

    allow_threads = False   # TODO

    answer['tuple_format'] = ', '.join([ '"' + tuple_format + '"' ] + tuple_target)
    answer['allow_threads'] = allow_threads

    if len(call_args) > 0:
        answer['call_args'] = ', ' + ', '.join(call_args)

    if not allow_threads:
        print('Threads are not allowed in function: %s()' % f['c_function'])

    return answer



globalFunctions = []

for f in model['global_functions']:
    g = FormatFunction(f)
    if g != None:
        globalFunctions.append(g)

for f in CUSTOM_FUNCTIONS:
    g = FormatFunction(f)
    if g != None:
        globalFunctions.append(g)


enumerations = []

with open(os.path.join(ROOT, 'Enumeration.mustache'), 'r') as f:
    ENUMERATION_TEMPLATE = f.read()

for e in model['enumerations']:
    values = []
    for value in e['values']:
        values.append({
            'key' : ToUpperCase(value['key']),
            'value' : value['value'],
        })

    enumerations.append({
        'name' : e['name'],
        'path' : 'sdk_%s.impl.h' % e['name'],
        'values' : values,
    })

    path = 'sdk_%s.impl.h' % e['name']

    with open(os.path.join(TARGET, path), 'w') as f:
        f.write(pystache.render(ENUMERATION_TEMPLATE, {
            'name' : e['name'],
            'short_name' : GetShortName(e['name']),
            'values' : values,
        }))


classes = []

for c in model['classes']:
    methods = []

    for m in c['methods']:
        g = FormatFunction(m)
        if g != None:
            g['self'] = ', self->object_'
            methods.append(g)

    classes.append({
        'class_name' : c['name'],
        'short_name' : GetShortName(c['name']),
        'methods' : methods,
    })

    if c['name'] in CUSTOM_METHODS:
        classes[-1]['custom_methods'] = CUSTOM_METHODS[c['name']]

    if 'destructor' in c:
        classes[-1]['destructor'] = c['destructor']




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 c in classes:
            with open(os.path.join(TARGET, 'sdk_%s.impl.h' % c['class_name']), 'w') as h:
                h.write(renderer.render(classDefinition, c))
            with open(os.path.join(TARGET, 'sdk_%s.methods.h' % c['class_name']), 'w') as h:
                h.write(renderer.render(classMethods, c))


sortedClasses = sorted(classes, key = lambda x: x['class_name'])
sortedEnumerations = sorted(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,
        }))