iCAD SX用のPythonパッケージを作ってみた

2024/03/13 categories:iCAD SX| tags:iCAD SX|Python|

iCAD SXをPythonで扱うためのPythonパッケージを作ってみました。

作成したパッケージ

pysxnet-1.0.0.zip

作成したパッケージの中身

zipを解凍すると以下のような構成になっています。インストールのために解凍する必要はありません。

pysxnet    ← パッケージのフォルダ
├pysxnet    ← パッケージのコードを入れているフォルダ
│├__init__.py    ← importの目印的なやつ
│├_pysxnet.py    ← ここにコードを書いています
│└sxnet_dll_to_python_code.py    ← このコードでiCADのdllをpythonパッケージのコードに書き換えられます
└setup.py    ← pipでインストールできるようにするやつ

インストール

pipでzipファイルからインストールする要領で、下記のpipコマンドを使用してzipファイルをインストールします。

python -m pip install --no-index --find-links=pysxnet-1.0.0.zip pysxnet

使い方

pysxnetをimportして使います。下記画像のように、VScodeなどでシンタックスハイライトが有効になったり、入力の候補が表示されたりするようになります。というよりも、エディタのこれらの機能を使うためだけに作成した、ヘッダーファイル的なパッケージなので、このパッケージ独自の機能があるわけではありません。

C#ではsxnet.SxWF.getActive()みたいな感じで書いているようにスタティックメソッドになっていますが、コードの変換が面倒だったのですべてインスタンスメソッドとしてpythonコードに変換しています。従って、このパッケージを使用する場合は、sxnet.SxWF().getActive()というように、クラス名の後に()を付けてインスタンス化して使用する必要があります。

オーバーロードがあるメソッドの扱い

基本的にpythonではオーバーロードできないので、オーバーロードがあるメソッドは仕方がなくメソッド名の末尾に番号を付けています。引数がどれだったか調べなければいけなくなってしまい、かなり使いにくくなってしまいましたが、そのうちどうにかできないか考えようと思います。

dllをpythonコードに変換するコード

dllをpythonコードに変換するために、pythonでプログラムを作成しました。パッケージ内のsxnet_dll_to_python_code.pyがそのプログラムです。pythonnetで.netを使い、System.Reflectionでdllの構造をpythonのクラスに書き換えるようなプログラムです。コードは以下のような感じです。

import clr
import os
clr.AddReference('System')
clr.AddReference('System.Reflection')
import System
import System.Reflection


DOT_NET_TYPE_NAME = {
    'System.Boolean' : 'bool',
    'System.Void' : 'None',
    'System.Int128' : 'int',
    'System.Int12' : 'int',
    'System.Int32' : 'int',
    'System.Int64' : 'int',
    'System.Double' : 'float',
    'System.Object' : 'Any',
    'System.String' : 'str'
}


def main():
    icad_dir = os.getenv("ICADDIR")
    dll_apth = f'{icad_dir}\\bin\\sxnet.dll'

    asm = System.Reflection.Assembly.LoadFile(dll_apth)

    classes = {}

    for class_info in asm.GetTypes():

        if not class_info.IsClass:
            continue

        if not class_info.IsPublic:
            continue
        
        class_name, code = class_code(class_info)

        class_data = {
            'code' : code,
            'fields' : [],
            'properties' : [],
            'methods' : {}
        }

        for field_info in class_info.GetFields():
            if field_info.IsPublic:
                class_data['fields'].append( field_code(4, class_info, field_info) )

        for property_info in class_info.GetProperties():
            class_data['properties'].append( property_code(4, property_info) )

        for method_info in class_info.GetMethods():
            if method_info.IsPublic:
                count = 0
                for i in range(100):
                    if not f'{method_info.Name}{"" if i == 0 else i}' in class_data['methods']:
                        count = i
                        break
                method_name, code = method_code(4, class_info, method_info, count)
                if not method_name in ['Equals', 'GetHashCode', 'GetType', 'ToString']:
                    class_data['methods'][method_name] = code
        
        sorted_methods = dict(sorted(class_data['methods'].items()))
        class_data['methods'] = sorted_methods

        classes[class_name] = class_data

    classes = dict(sorted(classes.items()))

    codes = [
        'from __future__ import annotations',
        'import clr',
        'import os',
        'from typing import Any',
        '',
        "clr.AddReference(f'{os.getenv(\"ICADDIR\")}\\\\bin\\\\sxnet.dll')",
        "clr.AddReference('System')",
        'import sxnet',
        'import System',
        '',
        ''
    ]

    for class_data in classes.values():
        codes.append( class_data['code'] )
        codes.extend( class_data['fields'] )
        codes.extend( class_data['properties'] )
        codes.extend( class_data['methods'].values() )
        codes.extend(['', ''])

    with open('pysxnet/_pysxnet.py', mode='w', encoding='utf-8') as f:
        f.write('\n'.join(codes))

    codes = [ 'from pysxnet._pysxnet import (' ]
    codes.extend([ f'    {key},' for key in classes.keys() ])
    codes.extend([')', ''])

    with open('pysxnet/__init__.py', mode='w', encoding='utf-8') as f:
        f.write('\n'.join(codes))

def class_code(class_info):
    class_name = class_info.FullName.replace('sxnet.', '')
    base_name = class_info.BaseType.FullName
    if 'sxnet.' in base_name:
        base_name = base_name.replace('sxnet.', '')
        code = f'class {class_name}({base_name}):'
    else:
        code = f'class {class_name}:'
    return class_name, code

def field_code(indent, class_info, field_info):
    field_name = field_info.Name
    field_type = convert_type( field_info.FieldType.FullName )
    field_value = f'{class_info.FullName}.{field_name}'

    if field_info.IsStatic:
        code = f'{" "*indent}{field_name}: {field_type} = {field_value}'
    else:
        code = f'{" "*indent}{field_name}: {field_type}'

    return code

def property_code(indent, property_info):
    property_name = property_info.Name
    property_type = convert_type( property_info.PropertyType.FullName )
    code = f'{" "*indent}{property_name}: {property_type}'
    return code

def method_code(indent, class_info, method_info, count): 
    method_name = method_info.Name
    return_type = convert_type(method_info.ReturnType.FullName)

    args = ['self']
    return_process_args = []
    for arg in method_info.GetParameters():
        arg_type = arg.ParameterType.FullName
        arg_name = arg.Name if arg.Name != 'global' else '_global'
        args.append(f'{arg_name}: {convert_type(arg_type)}')
        return_process_args.append(arg_name)

    args = ', '.join(args)
    return_process_args = ', '.join(return_process_args)

    return_process = f'{class_info.FullName}.{method_name}({return_process_args})'

    method_name = method_info.Name if count == 0 else f'{method_info.Name}{count}'
    code = f'{" "*indent}def {method_name}({args}) -> {return_type}: return {return_process}'

    return method_name, code

def convert_type(type_string: str):
    for dot_name_type, python_type in DOT_NET_TYPE_NAME.items():
        if dot_name_type in type_string:
            type_string = type_string.replace(dot_name_type, python_type)
    type_string = type_string.replace('sxnet.', '')
    if '+' in type_string:
        type_string = type_string.replace('+', '.')
    if '[]' in type_string:
        type_string = f'list[{type_string.replace("[]", "")}]'
    return type_string


if __name__ == '__main__':
    main()

Share post

Related Posts

コメント