import sys
import io
from pathlib import Path
import json
import ctypes
import itertools
from typing import Union
from cslug.c_parse import parse_functions, parse_structs
from cslug import misc
from cslug._struct import make_struct
[docs]
class Types(object):
"""Manages type information which is not found in a |../shared library|.
* Scans source code for:
- The name, argument and return types of each function.
- The name, field names, types and bit-field sizes of structures.
* Stores the above in a portable and quickly deserializable json file.
* Sets the types for the contents of a `ctypes.CDLL`.
"""
[docs]
def __init__(self, path, *sources, headers=(), compact=True):
"""
Args:
path (str or os.PathLike or io.TextIOBase):
A filename to read or write serialised type information to.
*sources (str or os.PathLike or io.TextIOBase):
C sources to extract function definitions from.
headers (str or os.PathLike or io.TextIOBase or list):
C sources to extract function prototypes from.
compact (bool):
If true, serialise minimising file size. Otherwise, pretty
format for human readability.
Note the distinction between **sources** and **headers**.
A function prototype such as :c:`int foo();` will be ignored if
found in **sources** but included if it were in **headers**.
A true function definition such as :c:`int foo() {}`, as well as
structure definitions would be collected in either case.
"""
self.sources = [misc.as_path_or_buffer(i) for i in sources]
self.headers = list(map(misc.as_path_or_buffer, misc.flatten(headers)))
self.json_path = misc.as_path_or_buffer(path)
self.compact = compact
types: dict
"""All type information collected. This is broken out into `functions` and
`structs`.
Note that this attribute is not set automatically. You must explicitly call
either `init_from_source` or `init_from_json` before accessing.
"""
json_path: Union[io.TextIOBase, Path]
"""File to read or write the json."""
[docs]
def init_from_source(self):
"""Initialise `types` by scanning source code."""
self.types = self._types_from_source()
[docs]
def init_from_json(self):
"""Initialise `types` by source code."""
self.types = self._types_from_json()
def _types_from_source(self):
"""Read and parse all source files.
:rtype: dict
"""
functions = {}
structs = {}
for source in itertools.chain(self.sources, self.headers):
structs.update(parse_structs(misc.read(source)[0]))
for source in self.sources:
functions.update(
parse_functions(misc.read(source)[0], typedefs=structs))
for source in self.headers:
functions.update(
parse_functions(
misc.read(source)[0], typedefs=structs, prototypes=True))
return {"functions": functions, "structs": structs}
def _types_from_json(self):
return json.loads(misc.read(self.json_path)[0])
[docs]
def make(self):
"""Initialise from source then write to file."""
self.init_from_source()
self.write(self.json_path)
[docs]
def write(self, path=sys.stdout):
"""Serialise contents to **path**.
Args:
path (str or os.PathLike or io.TextIOBase):
A filename or stream to write to. Defaults to `sys.stdout`.
"""
if self.compact:
# Squeeze out redundant whitespace characters.
out = json.dumps(self.types, separators=(",", ":")),
else:
# Write a git friendly, human readable json.
out = json.dumps(self.types, indent=" ", sort_keys=True), "\n"
misc.write(path, *out)
# def __getattribute__(self, item):
# if item == "types":
# self.init()
# return super().__getattribute__(item)
@property
def functions(self) -> dict:
"""All functions (from true definitions of prototypes).
The format is::
function_name: [return_type, [arg_type, arg_type, ...]]
All types are strings - either names of structures or attribute names of
`ctypes`.
"""
return self.types["functions"]
@property
def structs(self) -> dict:
"""All structures defined using :c:`typedef struct {...} name`.
The format is::
name: [(field_name, field_type), ...]
Or for bit-field structs::
name: [(field_name, field_type, bit_length), ...]
"""
return self.types["structs"]
[docs]
def apply(self, dll, strict=False):
"""Set the type information for the contents of **dll**.
Args:
dll (ctypes.CDLL):
The opened |../shared library| to apply type information to.
strict (bool):
Raise an `AttributeError` if a symbol wasn't found.
For every structure in ``self.structs``, turn it into a
`ctypes.Structure` and set it as an attribute of **dll**. For every
function in ``self.functions``, get it from **dll** then set its
``argtypes`` and ``restype`` attributes.
.. note::
Structures don't normally go in |../shared libraries| but |cslug| lobs
them in there for simplicity.
"""
dll.__dict__.update(self._merge_apply(dll, strict=strict))
def _merge_apply(self, *dlls, strict=False):
structs = {}
if strict:
errors = []
for (name, params) in self.structs.items():
fields = [(name, getattr(ctypes, type), *bits)
for (name, type, *bits) in params]
structs[name] = make_struct(name, fields)
namespace = {}
for (name, (return_type, arg_types)) in self.functions.items():
for dll in dlls:
func = getattr(dll, name, None)
if func is not None:
break
else:
# A function may be missing if any of:
#
# - The function was declared `inline`.
# - The function was not `__dll_export`ed and the `--fPIC`
# build flag was not used.
# - CSlug screwed up (not that unlikely).
#
# Don't crash if this is the case.
if strict:
errors.append(name)
# The continue below gets ignored by coverage. Add this useless
# statement to ensure it's being ran.
else:
pass
continue # pragma: no cover
# Set function return type. Default to no return value.
func.restype = structs.get(return_type) \
or getattr(ctypes, return_type, None)
# Set argument types. Default to int. If this is wrong however this
# will almost certainly cause strange incorrect behaviour.
func.argtypes = [
structs.get(i) or getattr(ctypes, i, ctypes.c_int)
for i in arg_types
]
namespace[name] = func
if strict and len(errors):
dll = " ".join(map(repr, dlls))
raise AttributeError(f"Symbols {errors} not found in {dll}.")
namespace.update(structs)
return namespace
if __name__ == "__main__":
pass