Commit f3583bc0857eb61fd10dc7ee362174b8a5805fa1

Authored by Jay Berkenbilt
1 parent 9367eb8a

Use castxml on headers instead of special sizes.cc

Figuring out which classes are part of the public API by using library
symbols is fragile (dependent on specific compiler optimizations) and
unreliable (misses some inline things). Instead, use castxml, a tool
that parses C++ to an abstract syntax tree and generates XML, to get a
reliable accounting of public classes and their sizes.
CMakeLists.txt
... ... @@ -39,9 +39,6 @@ CMAKE_DEPENDENT_OPTION(
39 39 WERROR "Treat compiler warnings as errors" OFF
40 40 "NOT MAINTAINER_MODE; NOT CI_MODE" ON)
41 41 CMAKE_DEPENDENT_OPTION(
42   - CHECK_SIZES "Compare sizes.cc with classes in public API" OFF
43   - "NOT MAINTAINER_MODE" ON)
44   -CMAKE_DEPENDENT_OPTION(
45 42 GENERATE_AUTO_JOB "Automatically regenerate job files" OFF
46 43 "NOT MAINTAINER_MODE" ON)
47 44 CMAKE_DEPENDENT_OPTION(
... ...
README-maintainer.md
... ... @@ -862,6 +862,8 @@ RelWithDebInfo when using external-libs.
862 862  
863 863 ## ABI checks
864 864  
  865 +Note: the check_abi program requires [castxml](https://github.com/CastXML/CastXML) to be installed.
  866 +
865 867 Until the conversion of the build to cmake, we relied on running the
866 868 test suite with old executables and a new library. When QPDFJob was
867 869 introduced, this method got much less reliable since a lot of public
... ... @@ -891,9 +893,7 @@ still things that could potentially break ABI, such as
891 893 Not breaking ABI/API still requires care.
892 894  
893 895 The check_abi script is responsible for performing many of these
894   -steps. See comments in check_abi for additional notes. Running
895   -"check_abi check-sizes" is run by ctest on Linux when CHECK_SIZES is
896   -on.
  896 +steps. See comments in check_abi for additional notes.
897 897  
898 898 ## CODE FORMATTING
899 899  
... ...
abi-perf-test
... ... @@ -59,7 +59,7 @@ cmake --build build -j$(nproc)
59 59  
60 60 echo "** saving old library and size information **"
61 61  
62   -./build/qpdf/sizes >| $work/old/sizes
  62 +$source/check_abi get-sizes --include include >| $work/old/sizes
63 63 cp build/libqpdf/libqpdf.so.*.* $work/old
64 64  
65 65 if [ "$SKIP_PERF" != "1" ]; then
... ... @@ -74,12 +74,11 @@ git checkout abi-new
74 74 cmake -S . -B build \
75 75 -DMAINTAINER_MODE=1 -DBUILD_STATIC_LIBS=0 -DBUILD_DOC=0 \
76 76 -DCMAKE_BUILD_TYPE=RelWithDebInfo
77   -cmake --build build -j$(nproc) --target sizes
  77 +cmake --build build -j$(nproc) --target libqpdf
78 78  
79 79 echo "** saving new library and size information **"
80 80  
81   -$source/check_abi check-sizes --lib build/libqpdf/libqpdf.so
82   -./build/qpdf/sizes >| $work/new/sizes
  81 +$source/check_abi get-sizes --include include >| $work/new/sizes
83 82 cp build/libqpdf/libqpdf.so.*.* $work/new
84 83  
85 84 echo "** running ABI comparison **"
... ...
cSpell.json
... ... @@ -61,6 +61,7 @@
61 61 "bufsize",
62 62 "buildrules",
63 63 "calledgetallpages",
  64 + "castxml",
64 65 "ccase",
65 66 "ccitt",
66 67 "cdef",
... ...
check_abi
... ... @@ -4,9 +4,10 @@ import sys
4 4 import argparse
5 5 import subprocess
6 6 import re
  7 +import xml.etree.ElementTree as ET
  8 +from operator import itemgetter
7 9  
8 10 whoami = os.path.basename(sys.argv[0])
9   -whereami = os.path.dirname(os.path.realpath(__file__))
10 11  
11 12  
12 13 def warn(*args, **kwargs):
... ... @@ -18,8 +19,8 @@ class Main:
18 19 options = self.parse_args(args, prog)
19 20 if options.action == 'dump':
20 21 self.dump(options)
21   - elif options.action == 'check-sizes':
22   - self.check_sizes(options)
  22 + elif options.action == 'get-sizes':
  23 + self.get_sizes(options)
23 24 elif options.action == 'compare':
24 25 self.compare(options)
25 26 else:
... ... @@ -43,10 +44,12 @@ class Main:
43 44 help='dump qpdf symbols in a library')
44 45 p_dump.add_argument(lib_arg[0], **lib_arg[1])
45 46  
46   - p_check_sizes = subparsers.add_parser(
47   - 'check-sizes',
48   - help='check consistency between library and sizes.cc')
49   - p_check_sizes.add_argument(lib_arg[0], **lib_arg[1])
  47 + p_get_sizes = subparsers.add_parser(
  48 + 'get-sizes',
  49 + help='dump sizes of all public classes')
  50 + p_get_sizes.add_argument('--include',
  51 + help='include path',
  52 + required=True)
50 53  
51 54 p_compare = subparsers.add_parser(
52 55 'compare',
... ... @@ -103,60 +106,73 @@ class Main:
103 106 for i in sorted(self.get_symbols(options.lib)):
104 107 print(i)
105 108  
106   - def check_sizes(self, options):
107   - # Make sure that every class with methods in the public API
108   - # appears in sizes.cc either explicitly ignored or in a
109   - # print_size call. This enables us to reliably test whether
110   - # any object changed size by following the ABI checking
111   - # procedures outlined in README-maintainer.
112   -
113   - # To keep things up to date, whenever we add or remove
114   - # objects, we have to update sizes.cc. The check-sizes option
115   - # can be run at any time on an up-to-date build.
116   -
117   - lib = self.get_symbols(options.lib)
118   - classes = set()
119   - for i in sorted(lib):
120   - # Find a symbol that looks like a class method.
121   - m = re.match(
122   - r'(((?:^\S*?::)?(?:[^:\s]+))::([^:\s]+))(?:\[[^\]]+\])?\(',
123   - i)
124   - if m:
125   - full = m.group(1)
126   - clas = m.group(2)
127   - method = m.group(3)
128   - if full.startswith('std::') or method.startswith('~'):
129   - # Sometimes std:: template instantiations make it
130   - # into the library. Ignore those. Also ignore
131   - # classes whose only exported method is a
132   - # destructor.
133   - continue
134   - # Otherwise, if the class exports a method, we
135   - # potentially care about changes to its size, so add
136   - # it.
137   - classes.add(clas)
138   - in_sizes = set()
139   - # Read the sizes.cc to make sure everything's there.
140   - with open(os.path.join(whereami, 'qpdf/sizes.cc'), 'r') as f:
141   - for line in f.readlines():
142   - m = re.search(r'^\s*(?:ignore_class|print_size)\((.*?)\)',
143   - line)
144   - if m:
145   - in_sizes.add(m.group(1))
146   - sizes_only = in_sizes - classes
147   - classes_only = classes - in_sizes
148   - if sizes_only or classes_only:
149   - if sizes_only:
150   - print("classes in sizes.cc but not in the library:")
151   - for i in sorted(sizes_only):
152   - print(' ', i)
153   - if classes_only:
154   - print("classes in the library but not in sizes.cc:")
155   - for i in sorted(classes_only):
156   - print(' ', i)
157   - exit(f'{whoami}: mismatch between library and sizes.cc')
158   - else:
159   - print(f'{whoami}: sizes.cc is consistent with the library')
  109 + @staticmethod
  110 + def get_header_sizes(include, filename):
  111 + print(f'{filename}...', file=sys.stderr)
  112 + p = subprocess.run([
  113 + 'castxml', '--castxml-output=1', f'-I{include}',
  114 + f'{include}/{filename}', '-o', '-',
  115 + ], stdout=subprocess.PIPE, check=True)
  116 + tree = ET.fromstring(p.stdout)
  117 + this_file = tree.find(f'.//File[@name="{include}/{filename}"]')
  118 + if this_file is None:
  119 + # This file doesn't define anything, e.g., DLL.h
  120 + return []
  121 + this_file_id = this_file.attrib["id"]
  122 + wanted = [
  123 + 'Namespace',
  124 + 'Struct', 'Union', 'Class', # records
  125 + ]
  126 +
  127 + by_id = {}
  128 + for elem in tree:
  129 + # Reference
  130 + # https://github.com/CastXML/CastXML/blob/master/doc/manual/castxml.xsd
  131 + if elem.tag not in wanted:
  132 + continue
  133 + is_record = elem.tag != 'Namespace'
  134 + record = {
  135 + 'is_record': is_record,
  136 + 'in_file': (elem.get('file') == this_file_id and
  137 + elem.get('incomplete') is None),
  138 + 'name': elem.get('name'),
  139 + 'access': elem.get('access'),
  140 + 'context': elem.get('context'),
  141 + 'size': elem.get('size'),
  142 + }
  143 + by_id[elem.attrib['id']] = record
  144 + classes = []
  145 + for id_, record in by_id.items():
  146 + cur = record
  147 + if not cur['in_file']:
  148 + continue
  149 + name = ''
  150 + private = False
  151 + while cur is not None:
  152 + if cur.get('access') == 'private':
  153 + private = True
  154 + parent = cur.get('context')
  155 + name = f'{cur["name"]}{name}'
  156 + if parent is None or parent == '_1':
  157 + break
  158 + name = f'::{name}'
  159 + cur = by_id.get(cur.get('context'))
  160 + if not private:
  161 + classes.append([name, record['size']])
  162 + return classes
  163 +
  164 + def get_sizes(self, options):
  165 + classes = []
  166 + for f in os.listdir(f'{options.include}/qpdf'):
  167 + if f.startswith('auto_') or f == 'QPDFObject.hh':
  168 + continue
  169 + if f.endswith('.h') or f.endswith('.hh'):
  170 + classes.extend(self.get_header_sizes(
  171 + options.include, f"qpdf/{f}"))
  172 +
  173 + classes.sort(key=itemgetter(0))
  174 + for c, s in classes:
  175 + print(c, s)
160 176  
161 177 def read_sizes(self, filename):
162 178 sizes = {}
... ... @@ -217,6 +233,12 @@ class Main:
217 233  
218 234 if __name__ == '__main__':
219 235 try:
  236 + subprocess.run(['castxml', '--version'],
  237 + stdout=subprocess.DEVNULL,
  238 + stderr=subprocess.DEVNULL)
  239 + except Exception:
  240 + exit('f{whoami}: castxml must be installed')
  241 + try:
220 242 Main().main()
221 243 except KeyboardInterrupt:
222 244 exit(130)
... ...
job.sums
1 1 # Generated by generate_auto_job
2   -CMakeLists.txt f0819695e4867e4f4389d38b0c124e79aa3ec9ace50f16ad8c751ff7f1ec6690
  2 +CMakeLists.txt 88e8974a8b14e10c941a4bb04ff078c3d3063b98af3ea056e02b1dcdff783d22
3 3 generate_auto_job f64733b79dcee5a0e3e8ccc6976448e8ddf0e8b6529987a66a7d3ab2ebc10a86
4 4 include/qpdf/auto_job_c_att.hh 4c2b171ea00531db54720bf49a43f8b34481586ae7fb6cbf225099ee42bc5bb4
5 5 include/qpdf/auto_job_c_copy_att.hh 50609012bff14fd82f0649185940d617d05d530cdc522185c7f3920a561ccb42
... ...
libqpdf/CMakeLists.txt
... ... @@ -599,13 +599,3 @@ if(INSTALL_CMAKE_PACKAGE)
599 599 COMPONENT ${COMPONENT_DEV}
600 600 DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/qpdf)
601 601 endif()
602   -
603   -if(CHECK_SIZES AND BUILD_SHARED_LIBS AND (CMAKE_SYSTEM_NAME STREQUAL "Linux"))
604   - # We can only do this check on a system with ELF shared libraries.
605   - # Since this is a maintainer-only option, testing for Linux is a
606   - # close enough approximation.
607   - add_test(
608   - NAME check-sizes
609   - COMMAND ${qpdf_SOURCE_DIR}/check_abi check-sizes
610   - --lib $<TARGET_FILE:libqpdf>)
611   -endif()
... ...
manual/installation.rst
... ... @@ -80,6 +80,13 @@ made because developers have to set the environment variable
80 80 themselves now rather than setting it through the build. Either way,
81 81 they are off by default.
82 82  
  83 +Maintainer Dependencies
  84 +~~~~~~~~~~~~~~~~~~~~~~~
  85 +
  86 +- To run ABI checks as a maintainer, you need `castxml
  87 + <https://github.com/CastXML/CastXML>`__, which is used by
  88 + ``check_abi`` to generate sizes of all public classes.
  89 +
83 90 Additional Requirements on Windows
84 91 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
85 92  
... ... @@ -301,14 +308,6 @@ ZOPFLI
301 308 Options for Working on qpdf
302 309 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
303 310  
304   -CHECK_SIZES
305   - The source file :file:`qpdf/sizes.cc` is used to display the sizes
306   - of all objects in the public API. Consistency of its output between
307   - releases is used as part of the check against accidental breakage of
308   - the binary interface (ABI). Turning this on causes a test to be run
309   - that ensures an exact match between classes in ``sizes.cc`` and
310   - classes in the library's public API. This option requires Python 3.
311   -
312 311 ENABLE_COVERAGE
313 312 Compile with ``--coverage``. See README-maintainer.md for
314 313 information about generating coverage reports.
... ... @@ -361,8 +360,6 @@ MAINTAINER_MODE
361 360  
362 361 - ``BUILD_DOC``
363 362  
364   - - ``CHECK_SIZES``
365   -
366 363 - ``ENABLE_QTC``
367 364  
368 365 - ``GENERATE_AUTO_JOB``
... ...
manual/release-notes.rst
... ... @@ -132,6 +132,10 @@ more detail.
132 132 - There has also been significant refactoring of how qpdf internally iterates over
133 133 arrays and dictionaries.
134 134  
  135 + - The internal mechanism used to check object sizes for binary
  136 + compatibility between releases has been changed. As such, the
  137 + ``CHECK_SIZES`` maintainer-only build option has been removed.
  138 +
135 139  
136 140 11.10.1: February 15, 2025
137 141 - Build fixes
... ...
qpdf/CMakeLists.txt
... ... @@ -2,7 +2,6 @@ set(MAIN_CXX_PROGRAMS
2 2 qpdf
3 3 fix-qdf
4 4 pdf_from_scratch
5   - sizes
6 5 test_char_sign
7 6 test_driver
8 7 test_large_file
... ... @@ -28,7 +27,6 @@ foreach(PROG ${MAIN_C_PROGRAMS})
28 27 target_link_libraries(${PROG} libqpdf)
29 28 set_property(TARGET ${PROG} PROPERTY LINKER_LANGUAGE CXX)
30 29 endforeach()
31   -target_include_directories(sizes PRIVATE ${JPEG_INCLUDE})
32 30  
33 31 set(needs_private_headers
34 32 test_large_file
... ...
qpdf/sizes.cc deleted
1   -// See "ABI checks" in README-maintainer and comments in check_abi.
2   -
3   -#include <iostream>
4   -
5   -#include <qpdf/Buffer.hh>
6   -#include <qpdf/BufferInputSource.hh>
7   -#include <qpdf/ClosedFileInputSource.hh>
8   -#include <qpdf/FileInputSource.hh>
9   -#include <qpdf/InputSource.hh>
10   -#include <qpdf/JSON.hh>
11   -#include <qpdf/PDFVersion.hh>
12   -#include <qpdf/Pipeline.hh>
13   -#include <qpdf/Pl_Buffer.hh>
14   -#include <qpdf/Pl_Concatenate.hh>
15   -#include <qpdf/Pl_Count.hh>
16   -#include <qpdf/Pl_DCT.hh>
17   -#include <qpdf/Pl_Discard.hh>
18   -#include <qpdf/Pl_Flate.hh>
19   -#include <qpdf/Pl_Function.hh>
20   -#include <qpdf/Pl_OStream.hh>
21   -#include <qpdf/Pl_QPDFTokenizer.hh>
22   -#include <qpdf/Pl_RunLength.hh>
23   -#include <qpdf/Pl_StdioFile.hh>
24   -#include <qpdf/Pl_String.hh>
25   -#include <qpdf/QPDF.hh>
26   -#include <qpdf/QPDFAcroFormDocumentHelper.hh>
27   -#include <qpdf/QPDFAnnotationObjectHelper.hh>
28   -#include <qpdf/QPDFCryptoProvider.hh>
29   -#include <qpdf/QPDFEFStreamObjectHelper.hh>
30   -#include <qpdf/QPDFEmbeddedFileDocumentHelper.hh>
31   -#include <qpdf/QPDFExc.hh>
32   -#include <qpdf/QPDFFileSpecObjectHelper.hh>
33   -#include <qpdf/QPDFFormFieldObjectHelper.hh>
34   -#include <qpdf/QPDFJob.hh>
35   -#include <qpdf/QPDFLogger.hh>
36   -#include <qpdf/QPDFMatrix.hh>
37   -#include <qpdf/QPDFNameTreeObjectHelper.hh>
38   -#include <qpdf/QPDFNumberTreeObjectHelper.hh>
39   -#include <qpdf/QPDFObjGen.hh>
40   -#include <qpdf/QPDFObjectHandle.hh>
41   -#include <qpdf/QPDFOutlineDocumentHelper.hh>
42   -#include <qpdf/QPDFOutlineObjectHelper.hh>
43   -#include <qpdf/QPDFPageDocumentHelper.hh>
44   -#include <qpdf/QPDFPageLabelDocumentHelper.hh>
45   -#include <qpdf/QPDFPageObjectHelper.hh>
46   -#include <qpdf/QPDFStreamFilter.hh>
47   -#include <qpdf/QPDFSystemError.hh>
48   -#include <qpdf/QPDFTokenizer.hh>
49   -#include <qpdf/QPDFUsage.hh>
50   -#include <qpdf/QPDFWriter.hh>
51   -#include <qpdf/QPDFXRefEntry.hh>
52   -
53   -#define ignore_class(cls)
54   -#define print_size(cls) std::cout << #cls << " " << sizeof(cls) << std::endl
55   -
56   -// These are not classes
57   -// -------
58   -ignore_class(QUtil);
59   -ignore_class(QTC);
60   -
61   -int
62   -main()
63   -{
64   - // Print the size of every class in the public API. This file is
65   - // read by the check_abi script at the top of the repository as
66   - // part of the binary compatibility checks performed before each
67   - // release.
68   - print_size(Buffer);
69   - print_size(BufferInputSource);
70   - print_size(ClosedFileInputSource);
71   - print_size(FileInputSource);
72   - print_size(InputSource);
73   - print_size(JSON);
74   - print_size(PDFVersion);
75   - print_size(Pipeline);
76   - print_size(Pl_Buffer);
77   - print_size(Pl_Concatenate);
78   - print_size(Pl_Count);
79   - print_size(Pl_DCT);
80   - print_size(Pl_Discard);
81   - print_size(Pl_Flate);
82   - print_size(Pl_Function);
83   - print_size(Pl_OStream);
84   - print_size(Pl_QPDFTokenizer);
85   - print_size(Pl_RunLength);
86   - print_size(Pl_StdioFile);
87   - print_size(Pl_String);
88   - print_size(QPDF);
89   - print_size(QPDFAcroFormDocumentHelper);
90   - print_size(QPDFAnnotationObjectHelper);
91   - print_size(QPDFCryptoProvider);
92   - print_size(QPDFEFStreamObjectHelper);
93   - print_size(QPDFEmbeddedFileDocumentHelper);
94   - print_size(QPDFExc);
95   - print_size(QPDFFileSpecObjectHelper);
96   - print_size(QPDFFormFieldObjectHelper);
97   - print_size(QPDFJob);
98   - print_size(QPDFJob::AttConfig);
99   - print_size(QPDFJob::Config);
100   - print_size(QPDFJob::CopyAttConfig);
101   - print_size(QPDFJob::EncConfig);
102   - print_size(QPDFJob::PagesConfig);
103   - print_size(QPDFJob::UOConfig);
104   - print_size(QPDFJob::PageLabelsConfig);
105   - print_size(QPDFLogger);
106   - print_size(QPDFMatrix);
107   - print_size(QPDFNameTreeObjectHelper);
108   - print_size(QPDFNameTreeObjectHelper::iterator);
109   - print_size(QPDFNumberTreeObjectHelper);
110   - print_size(QPDFNumberTreeObjectHelper::iterator);
111   - print_size(QPDFObjectHandle);
112   - print_size(QPDFObjectHandle::ParserCallbacks);
113   - print_size(QPDFObjectHandle::QPDFArrayItems);
114   - print_size(QPDFObjectHandle::QPDFArrayItems::iterator);
115   - print_size(QPDFObjectHandle::QPDFDictItems);
116   - print_size(QPDFObjectHandle::QPDFDictItems::iterator);
117   - print_size(QPDFObjectHandle::StreamDataProvider);
118   - print_size(QPDFObjectHandle::TokenFilter);
119   - print_size(QPDFObjectHelper);
120   - print_size(QPDFOutlineDocumentHelper);
121   - print_size(QPDFOutlineObjectHelper);
122   - print_size(QPDFPageDocumentHelper);
123   - print_size(QPDFPageLabelDocumentHelper);
124   - print_size(QPDFPageObjectHelper);
125   - print_size(QPDFStreamFilter);
126   - print_size(QPDFSystemError);
127   - print_size(QPDFTokenizer);
128   - print_size(QPDFTokenizer::Token);
129   - print_size(QPDFUsage);
130   - print_size(QPDFWriter);
131   - print_size(QPDFWriter::FunctionProgressReporter);
132   - print_size(QPDFXRefEntry);
133   - return 0;
134   -}