Orthanc/OrthancFramework/Resources/CheckOrthancFrameworkSymbols.py
2025-06-23 19:07:37 +05:30

352 lines
12 KiB
Python
Executable File

#!/usr/bin/env python
# Orthanc - A Lightweight, RESTful DICOM Store
# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
# Department, University Hospital of Liege, Belgium
# Copyright (C) 2017-2023 Osimis S.A., Belgium
# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
# Copyright (C) 2021-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 Lesser 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see
# <http://www.gnu.org/licenses/>.
##
## This maintenance script detects all the public methods in the
## Orthanc framework that come with an inlined implementation in the
## header file. Such methods can break the ABI of the shared library,
## as the actual implementation might change over versions.
##
# Ubuntu 20.04:
# sudo apt-get install python-clang-6.0
# ./ParseWebAssemblyExports.py --libclang=libclang-6.0.so.1 ./Test.cpp
# Ubuntu 18.04:
# sudo apt-get install python-clang-4.0
# ./ParseWebAssemblyExports.py --libclang=libclang-4.0.so.1 ./Test.cpp
# Ubuntu 14.04:
# ./ParseWebAssemblyExports.py --libclang=libclang-3.6.so.1 ./Test.cpp
import os
import sys
import clang.cindex
import argparse
##
## Parse the command-line arguments
##
parser = argparse.ArgumentParser(description = 'Parse WebAssembly C++ source file, and create a basic JavaScript wrapper.')
parser.add_argument('--libclang',
default = '',
help = 'manually provides the path to the libclang shared library')
parser.add_argument('--target-cpp-size',
default = '',
help = 'where to store C++ source to display the size of each public class')
args = parser.parse_args()
if len(args.libclang) != 0:
clang.cindex.Config.set_library_file(args.libclang)
index = clang.cindex.Index.create()
ROOT = os.path.abspath(os.path.dirname(sys.argv[0]))
SOURCES = []
for root, dirs, files in os.walk(os.path.join(ROOT, '..', 'Sources')):
for name in files:
if (os.path.splitext(name)[1] == '.h' and
not name.endswith('.impl.h')):
SOURCES.append(os.path.join(root, name))
AMALGAMATION = '/tmp/CheckOrthancFrameworkSymbols.cpp'
with open(AMALGAMATION, 'w') as f:
f.write('#include "%s"\n' % os.path.join(ROOT, '..', 'Sources', 'OrthancFramework.h'))
for source in SOURCES:
f.write('#include "%s"\n' % source)
tu = index.parse(AMALGAMATION, [
'--std=c++11',
'-DORTHANC_BUILDING_FRAMEWORK_LIBRARY=1',
'-DORTHANC_BUILD_UNIT_TESTS=0',
'-DORTHANC_ENABLE_BASE64=1',
'-DORTHANC_ENABLE_CIVETWEB=1',
'-DORTHANC_ENABLE_CURL=1',
'-DORTHANC_ENABLE_DCMTK=1',
'-DORTHANC_ENABLE_DCMTK_JPEG=1',
'-DORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS=1',
'-DORTHANC_ENABLE_DCMTK_NETWORKING=1',
'-DORTHANC_ENABLE_DCMTK_TRANSCODING=1',
'-DORTHANC_ENABLE_JPEG=1',
'-DORTHANC_ENABLE_LOCALE=1',
'-DORTHANC_ENABLE_LOGGING=1',
'-DORTHANC_ENABLE_LOGGING_STDIO=0',
'-DORTHANC_ENABLE_LUA=1',
'-DORTHANC_ENABLE_MD5=1',
'-DORTHANC_ENABLE_MONGOOSE=1',
'-DORTHANC_ENABLE_PKCS11=1',
'-DORTHANC_ENABLE_PNG=1',
'-DORTHANC_ENABLE_PUGIXML=1',
'-DORTHANC_ENABLE_SQLITE=1',
'-DORTHANC_ENABLE_SSL=1',
'-DORTHANC_ENABLE_ZLIB=1',
'-DORTHANC_SANDBOXED=0',
'-DORTHANC_SQLITE_STANDALONE=0',
'-DORTHANC_SQLITE_VERSION=3027001',
'-I/usr/include/jsoncpp', # On Ubuntu 18.04
'-I/usr/include/lua5.3', # On Ubuntu 18.04
])
if len(tu.diagnostics) != 0:
for d in tu.diagnostics:
print(' ** %s' % d)
print('')
raise Exception('Error')
FILES = []
COUNT = 0
ALL_TYPES = []
def ReportProblem(message, fqn, cursor):
global FILES, COUNT
FILES.append(os.path.normpath(str(cursor.location.file)))
COUNT += 1
print('%s: %s::%s()' % (message, '::'.join(fqn), cursor.spelling))
def ExploreClass(child, fqn):
# Safety check
if (child.kind != clang.cindex.CursorKind.CLASS_DECL and
child.kind != clang.cindex.CursorKind.STRUCT_DECL):
raise Exception()
# Ignore forward declaration of classes
if not child.is_definition():
return
##
## Verify that the class is publicly exported (its visibility must
## be "default")
##
visible = False
for i in child.get_children():
if (i.kind == clang.cindex.CursorKind.VISIBILITY_ATTR and
i.spelling == 'default'):
visible = True
if not visible:
return
global ALL_TYPES
ALL_TYPES.append('::'.join(fqn))
##
## Ignore pure abstract interfaces, by checking the following
## criteria:
## - It must be a C++ class (not a struct)
## - It must start with "I"
## - All its methods must be pure virtual (abstract) and public
## - Its destructor must be public, virtual, and must do nothing
##
if (child.kind == clang.cindex.CursorKind.CLASS_DECL and
child.spelling[0] == 'I' and
child.spelling[1].isupper()):
abstract = True
isPublic = False
for i in child.get_children():
if i.kind == clang.cindex.CursorKind.VISIBILITY_ATTR: # "default"
pass
elif i.kind == clang.cindex.CursorKind.CXX_ACCESS_SPEC_DECL:
isPublic = (i.access_specifier == clang.cindex.AccessSpecifier.PUBLIC)
elif i.kind == clang.cindex.CursorKind.CXX_BASE_SPECIFIER:
if i.spelling != 'boost::noncopyable':
abstract = False
elif isPublic:
if i.kind == clang.cindex.CursorKind.CXX_METHOD:
if i.is_pure_virtual_method():
pass # pure virtual is ok
elif i.is_static_method():
# static method without an inline implementation is ok
for j in i.get_children():
if j.kind == clang.cindex.CursorKind.COMPOUND_STMT:
abstract = False
else:
abstract = False
elif (i.kind == clang.cindex.CursorKind.DESTRUCTOR and
i.is_virtual_method()):
# The destructor must be virtual, and must do nothing
c = list(i.get_children())
if (len(c) != 1 or
c[0].kind != clang.cindex.CursorKind.COMPOUND_STMT or
len(list(c[0].get_children())) != 0):
abstract = False
elif i.kind == clang.cindex.CursorKind.CLASS_DECL:
ExploreClass(i, fqn + [ i.spelling ])
elif (i.kind == clang.cindex.CursorKind.TYPEDEF_DECL or # Allow "typedef"
i.kind == clang.cindex.CursorKind.ENUM_DECL): # Allow enums
pass
else:
abstract = False
if abstract:
print('Detected a pure interface (this is fine): %s' % ('::'.join(fqn)))
else:
ReportProblem('Not a pure interface', fqn, child)
return
##
## We are facing a standard C++ class or struct
##
isPublic = (child.kind == clang.cindex.CursorKind.STRUCT_DECL)
membersCount = 0
membersSize = 0
for i in child.get_children():
if (i.kind == clang.cindex.CursorKind.VISIBILITY_ATTR or # "default"
i.kind == clang.cindex.CursorKind.CXX_BASE_SPECIFIER): # base class
pass
elif i.kind == clang.cindex.CursorKind.CXX_ACCESS_SPEC_DECL:
isPublic = (i.access_specifier == clang.cindex.AccessSpecifier.PUBLIC)
elif i.kind == clang.cindex.CursorKind.CLASS_DECL:
# This is a subclass
if isPublic:
ExploreClass(i, fqn + [ i.spelling ])
elif (i.kind == clang.cindex.CursorKind.CXX_METHOD or
i.kind == clang.cindex.CursorKind.CONSTRUCTOR or
i.kind == clang.cindex.CursorKind.DESTRUCTOR):
if isPublic:
hasImplementation = False
for j in i.get_children():
if j.kind == clang.cindex.CursorKind.COMPOUND_STMT:
hasImplementation = True
if hasImplementation:
ReportProblem('Exported public method with an implementation', fqn, i)
elif i.kind == clang.cindex.CursorKind.VAR_DECL:
raise Exception('Unsupported: %s, %s' % (i.kind, i.location))
elif i.kind == clang.cindex.CursorKind.FUNCTION_TEMPLATE:
# An inline function template is OK, as it is not added to
# a shared library, but compiled by the client of the library
if isPublic:
print('Detected a template function (this is fine, but avoid it as much as possible): %s' % ('::'.join(fqn + [ i.spelling ])))
hasImplementation = False
for j in i.get_children():
if j.kind == clang.cindex.CursorKind.COMPOUND_STMT:
hasImplementation = True
if not hasImplementation:
ReportProblem('Exported template function without an inline implementation', fqn, i)
elif (i.kind == clang.cindex.CursorKind.TYPEDEF_DECL or # Allow "typedef"
i.kind == clang.cindex.CursorKind.ENUM_DECL): # Allow enums
pass
elif i.kind == clang.cindex.CursorKind.FRIEND_DECL:
children = list(i.get_children())
if (isPublic and
(len(children) != 1 or
not children[0].displayname in [
# This is supported for ABI compatibility with Orthanc <= 1.8.0
'operator<<(std::ostream &, const Orthanc::DicomTag &)',
])):
raise Exception('Unsupported: %s, %s' % (i.kind, i.location))
elif i.kind == clang.cindex.CursorKind.FIELD_DECL:
# TODO
if i.type.get_size() > 0:
membersSize += i.type.get_size()
membersCount += 1
else:
if isPublic:
raise Exception('Unsupported: %s, %s' % (i.kind, i.location))
#print('Size of %s => (%d,%d)' % ('::'.join(fqn), membersCount, membersSize))
def ExploreNamespace(node, namespace):
for child in node.get_children():
fqn = namespace + [ child.spelling ]
if child.kind == clang.cindex.CursorKind.NAMESPACE:
ExploreNamespace(child, fqn)
elif (child.kind == clang.cindex.CursorKind.CLASS_DECL or
child.kind == clang.cindex.CursorKind.STRUCT_DECL):
ExploreClass(child, fqn)
elif child.kind == clang.cindex.CursorKind.FUNCTION_DECL:
visible = False
hasImplementation = False
for i in child.get_children():
if (i.kind == clang.cindex.CursorKind.VISIBILITY_ATTR and
i.spelling == 'default'):
visible = True
elif i.kind == clang.cindex.CursorKind.COMPOUND_STMT:
hasImplementation = True
if visible and hasImplementation:
ReportProblem('Exported public function with an implementation', fqn, i)
print('')
for node in tu.cursor.get_children():
if (node.kind == clang.cindex.CursorKind.NAMESPACE and
node.spelling == 'Orthanc'):
ExploreNamespace(node, [ 'Orthanc' ])
if args.target_cpp_size != '':
with open(args.target_cpp_size, 'w') as f:
for t in sorted(ALL_TYPES):
f.write(' printf("sizeof(::%s) == %%d\\n", static_cast<int>(sizeof(::%s)));\n' % (t, t))
print('\nTotal of possibly problematic methods: %d' % COUNT)
print('\nProblematic files:\n')
for i in sorted(list(set(FILES))):
print(i)
print('')