iCAD SX用のPythonパッケージを作ってみた
2024/03/13 categories:iCAD SX| tags:iCAD SX|Python|
iCAD SXをPythonで扱うためのPythonパッケージを作ってみました。
作成したパッケージ
作成したパッケージの中身
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()