2 PROJECT: ReactOS apisets generator
3 LICENSE: MIT (https://spdx.org/licenses/MIT)
4 PURPOSE: Create apiset forwarders based on Wine apisets
5 COPYRIGHT: Copyright 2017,2018 Mark Jansen (mark.jansen@reactos.org)
11 from collections
import defaultdict
15 SCRIPT_DIR
= os
.path
.dirname(os
.path
.realpath(__file__
))
19 IGNORE_OPTIONS
= ('-norelay', '-ret16', '-ret64', '-register', '-private',
20 '-noname', '-ordinal', '-i386', '-arch=', '-stub', '-version=')
22 # Figure these out later
23 FUNCTION_BLACKLIST
= [
24 # api-ms-win-crt-utility-l1-1-0_stubs.c(6):
25 # error C2169: '_abs64': intrinsic function, cannot be defined
27 '_byteswap_uint64', '_byteswap_ulong', '_byteswap_ushort',
33 '# This file is autogenerated by update.py\n',
39 'kernelbase': 'kernel32',
43 # These modules cannot be linked against in ROS, so forward it
44 'cfgmgr32': 'setupapi', # Forward everything
45 'wmi': 'advapi32', # Forward everything
48 class InvalidSpecError(Exception):
49 def __init__(self
, message
):
50 Exception.__init
__(self
, message
)
58 Any
= i386 | x86_64 | arm | arm64
77 def __init__(self
, initial
=none
):
81 self
._val |
= sum([Arch
.FROM_STR
[arch
] for arch
in text
.split(',')])
85 return (self
._val
& val
) != 0
89 for value
in Arch
.TO_STR
:
91 arch_str
.append(Arch
.TO_STR
[value
])
92 return ','.join(arch_str
)
95 return bin(self
._val
).count("1")
97 def __add__(self
, other
):
98 return Arch(self
._val | other
._val
) # pylint: disable=W0212
100 def __sub__(self
, other
):
101 return Arch(self
._val
& ~other
._val
) # pylint: disable=W0212
103 def __gt__(self
, other
):
104 return self
._val
> other
._val
# pylint: disable=W0212
106 def __lt__(self
, other
):
107 return self
._val
< other
._val
# pylint: disable=W0212
109 def __eq__(self
, other
):
110 return self
._val
== other
._val
# pylint: disable=W0212
112 def __ne__(self
, other
):
113 return not self
.__eq
__(other
)
116 class VersionExpr(object):
117 def __init__(self
, text
):
126 return '-version={}'.format(self
.text
)
130 class SpecEntry(object):
131 def __init__(self
, text
, spec
):
138 self
._forwarder
= None
144 self
.name
= self
._forwarder
[1]
146 def init(self
, text
):
147 tokens
= re
.split(r
'([\s\(\)#;])', text
.strip())
148 tokens
= [token
for token
in tokens
if token
and not token
.isspace()]
150 for comment
in ['#', ';']:
151 if comment
in tokens
:
152 idx
.append(tokens
.index(comment
))
155 tokens
= tokens
[:idx
[0]]
157 raise InvalidSpecError(text
)
158 self
._ord
= tokens
[0]
159 assert self
._ord
== '@' or self
._ord
.isdigit(), text
161 self
.callconv
= tokens
.pop(0)
162 self
.name
= tokens
.pop(0)
163 while self
.name
.startswith(IGNORE_OPTIONS
):
164 if self
.name
.startswith('-arch='):
165 self
.arch
.add(self
.name
[6:])
166 elif self
.name
.startswith('-version='):
167 self
.version
= VersionExpr(self
.name
[9:])
168 elif self
.name
== '-i386':
169 self
.arch
.add('i386')
170 self
.name
= tokens
.pop(0)
172 self
.arch
= Arch(Arch
.Any
)
173 assert not self
.name
.startswith('-'), text
177 assert ')' in tokens
, text
185 assert len(tokens
) == 1, text
186 self
._forwarder
= tokens
.pop(0).split('.', 2)
187 if len(self
._forwarder
) == 1:
188 self
._forwarder
= ['self', self
._forwarder
[0]]
189 assert len(self
._forwarder
) in (0, 2), self
._forwarder
190 if self
._forwarder
[0] in ALIAS_DLL
:
191 self
._forwarder
[0] = ALIAS_DLL
[self
._forwarder
[0]]
193 def resolve_forwarders(self
, module_lookup
, try_modules
):
195 assert self
._forwarder
[1] == self
.name
, '{}:{}'.format(self
._forwarder
[1], self
.name
)
196 if self
.noname
and self
.name
== '@':
197 return 0 # cannot search for this function
200 for module_name
in try_modules
:
201 assert module_name
in module_lookup
, module_name
202 module
= module_lookup
[module_name
]
203 fwd_arch
= module
.find_arch(self
.name
)
204 callconv
= module
.find_callconv(self
.name
)
205 version
= module
.find_version(self
.name
)
208 self
._forwarder
= [module_name
, self
.name
]
209 self
.callconv
= callconv
210 self
.version
= version
214 def extra_forwarders(self
, function_lookup
, module_lookup
):
217 if self
.noname
and self
.name
== '@':
218 return 0 # cannot search for this function
219 lst
= function_lookup
.get(self
.name
, None)
221 modules
= list(set([func
.spec
.name
for func
in lst
]))
225 for module
in modules
:
226 mod_arch
= module_lookup
[module
].find_arch(self
.name
)
227 if mod
is None or mod_arch
> arch
:
232 self
._forwarder
= [mod
, self
.name
]
233 mod
= module_lookup
[mod
]
234 self
.arch
= mod
.find_arch(self
.name
)
235 self
.callconv
= mod
.find_callconv(self
.name
)
236 self
.version
= mod
.find_version(self
.name
)
240 def forwarder_module(self
):
242 return self
._forwarder
[0]
249 def write(self
, spec_file
):
254 opts
= '{} -noname'.format(opts
)
256 opts
= '{} {}'.format(opts
, self
.version
.to_str())
258 assert self
._ord
!= '@'
259 name
= 'Ordinal' + self
._ord
260 if not self
._forwarder
:
261 spec_file
.write('{} stub{} {}{}'.format(self
._ord
, opts
, name
, NL_CHAR
))
262 estimate_size
+= 0x1000
264 assert self
.arch
!= Arch(), self
.name
267 fwd
= '.'.join(self
._forwarder
)
268 name
= self
.name
if not self
.noname
else '@'
270 if self
.callconv
== 'extern':
272 callconv
= 'extern -stub' # HACK
273 fwd
+= ' # the -stub is a HACK to fix VS < 2017 build!'
274 if arch
!= Arch(Arch
.Any
):
275 opts
= '{} -arch={}'.format(opts
, arch
.to_str())
276 spec_file
.write('{ord} {cc}{opts} {name}{args} {fwd}{nl}'.format(ord=self
._ord
,
283 estimate_size
+= 0x100
288 class SpecFile(object):
289 def __init__(self
, fullpath
, name
):
290 self
._path
= fullpath
293 self
._functions
= defaultdict(list)
294 self
._estimate
_size
= 0
297 with
open(self
._path
, 'rb') as specfile
:
298 for line
in specfile
.readlines():
301 entry
= SpecEntry(line
, self
)
302 self
._entries
.append(entry
)
303 self
._functions
[entry
.name
].append(entry
)
304 except InvalidSpecError
:
306 return (sum([entry
.forwarder() for entry
in self
._entries
]), len(self
._entries
))
308 def add_functions(self
, function_lookup
):
309 for entry
in self
._entries
:
310 function_lookup
[entry
.name
].append(entry
)
312 def find(self
, name
):
313 return self
._functions
.get(name
, None)
315 def find_arch(self
, name
):
316 functions
= self
.find(name
)
319 for func
in functions
:
323 def find_callconv(self
, name
):
324 functions
= self
.find(name
)
327 for func
in functions
:
329 callconv
= func
.callconv
330 elif callconv
!= func
.callconv
:
331 assert callconv
!= 'extern', 'Cannot have data/function with same name'
332 callconv
= func
.callconv
335 def find_version(self
, name
):
336 functions
= self
.find(name
)
339 for func
in functions
:
343 assert version
.text
== func
.version
.text
344 version
= func
.version
347 def resolve_forwarders(self
, module_lookup
):
348 modules
= self
.forwarder_modules()
350 for entry
in self
._entries
:
351 total
+= entry
.resolve_forwarders(module_lookup
, modules
)
352 return (total
, len(self
._entries
))
354 def extra_forwarders(self
, function_lookup
, module_lookup
):
356 for entry
in self
._entries
:
357 total
+= entry
.extra_forwarders(function_lookup
, module_lookup
)
358 return (total
, len(self
._entries
))
360 def forwarder_modules(self
):
361 modules
= defaultdict(int)
362 for entry
in self
._entries
:
363 module
= entry
.forwarder_module()
366 return sorted(modules
, key
=modules
.get
, reverse
=True)
368 def write(self
, spec_file
):
369 written
= set(FUNCTION_BLACKLIST
)
370 self
._estimate
_size
= 0
371 for entry
in self
._entries
:
372 if entry
.name
not in written
:
373 self
._estimate
_size
+= entry
.write(spec_file
)
374 written
.add(entry
.name
)
376 def write_cmake(self
, cmakelists
, baseaddress
):
378 # ntdll and kernel32 are linked against everything, self = internal,
379 # we cannot link cfgmgr32 and wmi?
380 ignore
= ['ntdll', 'kernel32', 'self', 'cfgmgr32', 'wmi']
381 forwarders
= self
.forwarder_modules()
382 fwd_strings
= [x
for x
in forwarders
if not (x
in seen
or x
in ignore
or seen
.add(x
))]
383 fwd_strings
= ' '.join(fwd_strings
)
385 baseaddress
= '0x{:8x}'.format(baseaddress
)
386 cmakelists
.write('add_apiset({} {} {}){}'.format(name
, baseaddress
, fwd_strings
, NL_CHAR
))
387 return self
._estimate
_size
391 def generate_specnames(dll_dir
):
392 win32
= os
.path
.join(dll_dir
, 'win32')
393 for dirname
in os
.listdir(win32
):
394 fullpath
= os
.path
.join(win32
, dirname
, dirname
+ '.spec')
395 if not os
.path
.isfile(fullpath
):
397 fullpath
= os
.path
.join(win32
, dirname
, dirname
.rsplit('.', 1)[0] + '.spec')
398 if not os
.path
.isfile(fullpath
):
402 yield (fullpath
, dirname
)
404 yield (os
.path
.join(dll_dir
, 'ntdll', 'def', 'ntdll.spec'), 'ntdll')
405 yield (os
.path
.join(dll_dir
, 'appcompat', 'apphelp', 'apphelp.spec'), 'apphelp')
406 yield (os
.path
.join(dll_dir
, '..', 'win32ss', 'user', 'user32', 'user32.spec'), 'user32')
407 yield (os
.path
.join(dll_dir
, '..', 'win32ss', 'gdi', 'gdi32', 'gdi32.spec'), 'gdi32')
415 function_lookup
= defaultdict(list)
417 version
= subprocess
.check_output(["git", "describe"], cwd
=wineroot
).strip()
419 print 'Reading Wine apisets for', version
420 wine_apiset_path
= os
.path
.join(wineroot
, 'dlls')
421 for dirname
in os
.listdir(wine_apiset_path
):
422 if not dirname
.startswith('api-'):
424 if not os
.path
.isdir(os
.path
.join(wine_apiset_path
, dirname
)):
426 fullpath
= os
.path
.join(wine_apiset_path
, dirname
, dirname
+ '.spec')
427 spec
= SpecFile(fullpath
, dirname
)
428 wine_apisets
.append(spec
)
430 print 'Parsing Wine apisets,',
432 for apiset
in wine_apisets
:
433 total
= tuple(map(sum, zip(apiset
.parse(), total
)))
434 print 'found', total
[0], '/', total
[1], 'forwarders'
436 print 'Reading ReactOS modules'
437 for fullpath
, dllname
in generate_specnames(os
.path
.dirname(SCRIPT_DIR
)):
438 spec
= SpecFile(fullpath
, dllname
)
439 ros_modules
.append(spec
)
441 print 'Parsing ReactOS modules'
442 for module
in ros_modules
:
444 assert module
.name
not in module_lookup
, module
.name
445 module_lookup
[module
.name
] = module
446 module
.add_functions(function_lookup
)
448 print 'First pass, resolving forwarders,',
450 for apiset
in wine_apisets
:
451 total
= tuple(map(sum, zip(apiset
.resolve_forwarders(module_lookup
), total
)))
452 print 'found', total
[0], '/', total
[1], 'forwarders'
454 print 'Second pass, searching extra forwarders,',
456 for apiset
in wine_apisets
:
457 total
= tuple(map(sum, zip(apiset
.extra_forwarders(function_lookup
, module_lookup
), total
)))
458 print 'found', total
[0], '/', total
[1], 'forwarders'
460 with
open(os
.path
.join(SCRIPT_DIR
, 'CMakeLists.txt.in'), 'rb') as template
:
461 data
= template
.read()
462 data
= data
.replace('%WINE_GIT_VERSION%', version
)
463 # Detect the checkout newline settings
467 print 'Writing apisets'
468 spec_header
= [line
.replace('\n', NL_CHAR
) for line
in SPEC_HEADER
]
469 for apiset
in wine_apisets
:
470 with
open(os
.path
.join(SCRIPT_DIR
, apiset
.name
+ '.spec'), 'wb') as out_spec
:
471 out_spec
.writelines(spec_header
)
472 apiset
.write(out_spec
)
474 print 'Writing CMakeLists.txt'
475 baseaddress
= 0x60000000
476 with
open(os
.path
.join(SCRIPT_DIR
, 'CMakeLists.txt'), 'wb') as cmakelists
:
477 cmakelists
.write(data
)
478 for apiset
in wine_apisets
:
479 baseaddress
+= apiset
.write_cmake(cmakelists
, baseaddress
)
480 baseaddress
+= (0x10000 - baseaddress
) % 0x10000
488 print 'No path specified,'
489 print 'either pass it as argument, or set the environment variable "WINE_SRC_ROOT"'
491 if __name__
== '__main__':
492 main(sys
.argv
[1:] + [os
.environ
.get('WINE_SRC_ROOT')])