352 lines
12 KiB
Python
Executable File
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('')
|