|
| 1 | +""" |
| 2 | +CFFI wrapper for ScienceMode library. |
| 3 | +
|
| 4 | +This module uses CFFI to provide Python bindings for the ScienceMode library. |
| 5 | +It handles the compilation, linking, and loading of the C library, and provides |
| 6 | +utility functions for working with the CFFI interface. |
| 7 | +
|
| 8 | +The module offers: |
| 9 | +- Automatic header file and library detection |
| 10 | +- Platform-specific handling for Windows, macOS, and Linux |
| 11 | +- Resource management through context managers |
| 12 | +- Memory management helpers |
| 13 | +- Error handling utilities |
| 14 | +- String conversion functions |
| 15 | +""" |
| 16 | + |
1 | 17 | import glob |
2 | 18 | import itertools |
3 | 19 | import os |
|
10 | 26 | from pycparser import c_ast |
11 | 27 | from pycparser.c_generator import CGenerator |
12 | 28 |
|
| 29 | +# Regular expressions for parsing include paths and #define statements |
13 | 30 | INCLUDE_PATTERN = re.compile(r"(-I)?(.*ScienceMode)") |
14 | 31 | DEFINE_PATTERN = re.compile(r"^#define\s+(\w+)\s+\(?([\w<|.]+)\)?", re.M) |
| 32 | + |
| 33 | +# List of symbols that should not be processed as #define statements |
15 | 34 | DEFINE_BLACKLIST = { |
16 | 35 | "main", |
17 | 36 | } |
18 | 37 |
|
19 | 38 | # Try to find the include directory in different locations |
| 39 | +# We check multiple possible locations to support different installation structures |
20 | 40 | devel_root_candidates = [ |
21 | | - os.path.abspath("./smpt/ScienceMode_Library/include"), |
22 | | - os.path.abspath("./smpt/ScienceMode_Library"), |
23 | | - os.path.abspath("../smpt/ScienceMode_Library/include"), |
24 | | - os.path.abspath("../smpt/ScienceMode_Library"), |
25 | | - os.path.abspath("../include/ScienceMode4"), |
26 | | - os.path.abspath("./include/ScienceMode4"), |
| 41 | + os.path.abspath( |
| 42 | + "./smpt/ScienceMode_Library/include" |
| 43 | + ), # Standard source tree layout |
| 44 | + os.path.abspath("./smpt/ScienceMode_Library"), # Alternative source layout |
| 45 | + os.path.abspath("../smpt/ScienceMode_Library/include"), # When in a subdirectory |
| 46 | + os.path.abspath("../smpt/ScienceMode_Library"), # When in a subdirectory |
| 47 | + os.path.abspath("../include/ScienceMode4"), # Installed version layout |
| 48 | + os.path.abspath("./include/ScienceMode4"), # Installed version layout |
27 | 49 | ] |
28 | 50 |
|
29 | 51 | # Find the first valid include directory |
|
93 | 115 | smpt_include_path4 = include_dir |
94 | 116 |
|
95 | 117 | # define GCC specific compiler extensions away |
| 118 | +# Define preprocessor arguments for pycparser |
| 119 | +# These definitions help parse the header files correctly by: |
| 120 | +# 1. Setting the appropriate platform macros based on the current system |
| 121 | +# 2. Defining away GCC-specific extensions that pycparser can't handle |
| 122 | +# 3. Setting up include paths for both real headers and fake headers (for standard includes) |
96 | 123 | DEFINE_ARGS = [ |
97 | 124 | # Platform definitions - set according to current platform |
98 | 125 | # but make sure to handle platform-specific fields in structures |
|
134 | 161 | "-I" + smpt_include_path4, |
135 | 162 | ] |
136 | 163 |
|
137 | | -FUNCTION_BLACKLIST = {} |
| 164 | +# List of function names that should be excluded from processing |
| 165 | +FUNCTION_BLACKLIST = {} # Empty for now, but can be populated if needed |
138 | 166 |
|
139 | 167 | VARIADIC_ARG_PATTERN = re.compile(r"va_list \w+") |
140 | 168 | ARRAY_SIZEOF_PATTERN = re.compile(r"\[[^\]]*sizeof[^\]]*]") |
|
205 | 233 |
|
206 | 234 |
|
207 | 235 | class Collector(c_ast.NodeVisitor): |
| 236 | + """AST visitor that collects type declarations and function definitions. |
| 237 | +
|
| 238 | + This class walks through the Abstract Syntax Tree (AST) of C code and |
| 239 | + extracts all type declarations (structs, enums, typedefs) and function |
| 240 | + declarations for use with CFFI. |
| 241 | + """ |
| 242 | + |
208 | 243 | def __init__(self): |
209 | | - self.generator = CGenerator() |
210 | | - self.typedecls = [] |
211 | | - self.functions = [] |
| 244 | + """Initialize the collector with empty lists for types and functions.""" |
| 245 | + self.generator = CGenerator() # For generating C code from AST nodes |
| 246 | + self.typedecls = [] # Will hold all type declarations |
| 247 | + self.functions = [] # Will hold all function declarations |
212 | 248 |
|
213 | 249 | def process_typedecl(self, node): |
214 | 250 | coord = os.path.abspath(node.coord.file) |
@@ -263,6 +299,123 @@ def visit_FuncDecl(self, node): |
263 | 299 | ffi = FFI() |
264 | 300 |
|
265 | 301 |
|
| 302 | +# Function to initialize the library once |
| 303 | +def _init_smpt_lib(): |
| 304 | + """Initialize the ScienceMode library once during the program's lifetime.""" |
| 305 | + print("Initializing ScienceMode library...") |
| 306 | + # Any one-time initialization could go here |
| 307 | + return { |
| 308 | + "include_dir": include_dir, |
| 309 | + "lib_path": smpt_lib_path, |
| 310 | + "platform": platform.system(), |
| 311 | + } |
| 312 | + |
| 313 | + |
| 314 | +# Function to load library directly (useful for testing and debug) |
| 315 | +def load_library(): |
| 316 | + """Try to load the SMPT library directly using ffi.dlopen(). |
| 317 | +
|
| 318 | + This is useful for direct testing without building the extension module. |
| 319 | + Returns the loaded library or None if not found. |
| 320 | + """ |
| 321 | + # Choose library pattern based on platform |
| 322 | + if platform.system() == "Windows": |
| 323 | + patterns = ["smpt.dll", "libsmpt.dll"] |
| 324 | + elif platform.system() == "Darwin": |
| 325 | + patterns = ["libsmpt.dylib"] |
| 326 | + else: |
| 327 | + patterns = ["libsmpt.so"] |
| 328 | + |
| 329 | + # Try to load the library from the lib path |
| 330 | + for pattern in patterns: |
| 331 | + try: |
| 332 | + lib_path = os.path.join(smpt_lib_path, pattern) |
| 333 | + if os.path.exists(lib_path): |
| 334 | + print(f"Loading library from: {lib_path}") |
| 335 | + return ffi.dlopen(lib_path) |
| 336 | + except Exception as e: |
| 337 | + print(f"Failed to load {pattern}: {e}") |
| 338 | + if platform.system() == "Windows": |
| 339 | + try: |
| 340 | + # Use ctypes for Windows error handling |
| 341 | + import ctypes |
| 342 | + |
| 343 | + # Check if we're on Windows to safely use windll |
| 344 | + if sys.platform.startswith("win"): |
| 345 | + try: |
| 346 | + # First make sure windll is available |
| 347 | + if hasattr(ctypes, "windll"): |
| 348 | + # Get kernel32 if available |
| 349 | + if hasattr(ctypes.windll, "kernel32"): |
| 350 | + # Get the last error code if GetLastError is available |
| 351 | + if hasattr(ctypes.windll.kernel32, "GetLastError"): |
| 352 | + error_code = ( |
| 353 | + ctypes.windll.kernel32.GetLastError() |
| 354 | + ) |
| 355 | + print(f"Windows error code: {error_code}") |
| 356 | + |
| 357 | + # Try to get a formatted message if possible |
| 358 | + try: |
| 359 | + # Format message flags and parameters |
| 360 | + FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000 |
| 361 | + FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200 |
| 362 | + |
| 363 | + # Buffer for the error message |
| 364 | + buffer_size = 256 |
| 365 | + buffer = ctypes.create_string_buffer( |
| 366 | + buffer_size |
| 367 | + ) |
| 368 | + |
| 369 | + # Get the error message |
| 370 | + ctypes.windll.kernel32.FormatMessageA( |
| 371 | + FORMAT_MESSAGE_FROM_SYSTEM |
| 372 | + | FORMAT_MESSAGE_IGNORE_INSERTS, |
| 373 | + None, |
| 374 | + error_code, |
| 375 | + 0, |
| 376 | + buffer, |
| 377 | + buffer_size, |
| 378 | + None, |
| 379 | + ) |
| 380 | + |
| 381 | + # Convert the message to a Python string |
| 382 | + message = buffer.value.decode( |
| 383 | + "utf-8", errors="replace" |
| 384 | + ).strip() |
| 385 | + print(f"Windows error message: {message}") |
| 386 | + except Exception as msg_err: |
| 387 | + print( |
| 388 | + f"Error getting formatted error message: {msg_err}" |
| 389 | + ) |
| 390 | + else: |
| 391 | + print( |
| 392 | + "GetLastError function not available in kernel32" |
| 393 | + ) |
| 394 | + else: |
| 395 | + print("kernel32 not available in windll") |
| 396 | + else: |
| 397 | + print("windll not available in ctypes") |
| 398 | + except AttributeError: |
| 399 | + print( |
| 400 | + "Could not access Windows-specific ctypes functionality" |
| 401 | + ) |
| 402 | + else: |
| 403 | + print("Not on Windows, skipping windll usage") |
| 404 | + except Exception as win_err: |
| 405 | + print(f"Error getting Windows error details: {win_err}") |
| 406 | + else: |
| 407 | + try: |
| 408 | + if hasattr(ffi, "errno"): |
| 409 | + print(f"Error code: {ffi.errno}") |
| 410 | + else: |
| 411 | + print("Error occurred but errno not available") |
| 412 | + except Exception as err_ex: |
| 413 | + print(f"Error getting error code: {err_ex}") |
| 414 | + |
| 415 | + print("Could not load library directly") |
| 416 | + return None |
| 417 | + |
| 418 | + |
266 | 419 | ffi.set_source( |
267 | 420 | "sciencemode._sciencemode", |
268 | 421 | ("\n").join([f'#include "{header}"' for header in ROOT_HEADERS]), |
@@ -342,6 +495,171 @@ def visit_FuncDecl(self, node): |
342 | 495 |
|
343 | 496 | ffi.cdef(cdef) |
344 | 497 |
|
| 498 | + |
| 499 | +# Create a context manager class for managing CFFI resources |
| 500 | +class CFFIResourceManager: |
| 501 | + """Context manager for CFFI resources to ensure proper cleanup.""" |
| 502 | + |
| 503 | + def __init__(self, resource, destructor=None): |
| 504 | + """Initialize with a CFFI resource and optional destructor function. |
| 505 | +
|
| 506 | + Args: |
| 507 | + resource: CFFI resource to manage |
| 508 | + destructor: Optional function to call for cleanup |
| 509 | + """ |
| 510 | + self.resource = resource |
| 511 | + self.destructor = destructor |
| 512 | + |
| 513 | + def __enter__(self): |
| 514 | + """Return the resource when entering the context.""" |
| 515 | + return self.resource |
| 516 | + |
| 517 | + def __exit__(self, exc_type, exc_val, exc_tb): |
| 518 | + """Release the resource when exiting the context.""" |
| 519 | + if self.destructor and self.resource: |
| 520 | + try: |
| 521 | + self.destructor(self.resource) |
| 522 | + except Exception as e: |
| 523 | + print(f"Error during resource cleanup: {e}") |
| 524 | + elif hasattr(ffi, "release") and self.resource: |
| 525 | + try: |
| 526 | + ffi.release(self.resource) |
| 527 | + except Exception as e: |
| 528 | + print(f"Error releasing resource: {e}") |
| 529 | + self.resource = None |
| 530 | + return False # Don't suppress exceptions |
| 531 | + |
| 532 | + |
| 533 | +# Memory management helpers |
| 534 | +def managed_new(ctype, init=None, destructor=None, size=0): |
| 535 | + """Create a new CFFI object with automatic memory management. |
| 536 | +
|
| 537 | + Args: |
| 538 | + ctype: C type to allocate |
| 539 | + init: Initial value |
| 540 | + destructor: Custom destructor function (optional) |
| 541 | + size: Size hint for garbage collector (optional) |
| 542 | +
|
| 543 | + Returns: |
| 544 | + CFFI object with garbage collection |
| 545 | + """ |
| 546 | + if init is not None: |
| 547 | + obj = ffi.new(ctype, init) |
| 548 | + else: |
| 549 | + obj = ffi.new(ctype) |
| 550 | + |
| 551 | + if destructor: |
| 552 | + return ffi.gc(obj, destructor, size) |
| 553 | + return obj |
| 554 | + |
| 555 | + |
| 556 | +def managed_buffer(cdata, size=None): |
| 557 | + """Create a managed buffer from CFFI data. |
| 558 | +
|
| 559 | + Args: |
| 560 | + cdata: CFFI data to create buffer from |
| 561 | + size: Size of the buffer in bytes (optional) |
| 562 | +
|
| 563 | + Returns: |
| 564 | + A context manager that yields a buffer object |
| 565 | + """ |
| 566 | + buf = ffi.buffer(cdata, size) |
| 567 | + return CFFIResourceManager(buf) |
| 568 | + |
| 569 | + |
| 570 | +# Function to get the library configuration, initialized only once |
| 571 | +def get_smpt_config(): |
| 572 | + """Get the SMPT library configuration, initializing it only once. |
| 573 | +
|
| 574 | + Returns: |
| 575 | + dict: Configuration dictionary with paths and platform info |
| 576 | + """ |
| 577 | + return ffi.init_once(_init_smpt_lib, "smpt_init") |
| 578 | + |
| 579 | + |
| 580 | +# String conversion utilities |
| 581 | +def to_bytes(value): |
| 582 | + """Convert a Python string or bytes to bytes object. |
| 583 | +
|
| 584 | + Args: |
| 585 | + value: String or bytes to convert |
| 586 | +
|
| 587 | + Returns: |
| 588 | + bytes: Python bytes object |
| 589 | + """ |
| 590 | + if isinstance(value, str): |
| 591 | + return value.encode("utf-8") |
| 592 | + elif isinstance(value, bytes): |
| 593 | + return value |
| 594 | + else: |
| 595 | + return str(value).encode("utf-8") |
| 596 | + |
| 597 | + |
| 598 | +def from_cstring(cdata): |
| 599 | + """Convert a C string to a Python string. |
| 600 | +
|
| 601 | + Args: |
| 602 | + cdata: CFFI char* data |
| 603 | +
|
| 604 | + Returns: |
| 605 | + str: Python string |
| 606 | + """ |
| 607 | + if cdata == ffi.NULL: |
| 608 | + return None |
| 609 | + |
| 610 | + # Get the C string as bytes first |
| 611 | + byte_str = ffi.string(cdata) |
| 612 | + |
| 613 | + # Convert to Python string |
| 614 | + if isinstance(byte_str, bytes): |
| 615 | + return byte_str.decode("utf-8", errors="replace") |
| 616 | + return byte_str # Already a string in Python 3 |
| 617 | + |
| 618 | + |
| 619 | +def to_c_array(data_type, py_list): |
| 620 | + """Convert a Python list to a C array of the specified type. |
| 621 | +
|
| 622 | + Args: |
| 623 | + data_type: C data type (e.g., "int[]") |
| 624 | + py_list: Python list to convert |
| 625 | +
|
| 626 | + Returns: |
| 627 | + CFFI array object |
| 628 | + """ |
| 629 | + if not py_list: |
| 630 | + return ffi.NULL |
| 631 | + |
| 632 | + arr = ffi.new(data_type, len(py_list)) |
| 633 | + for i, value in enumerate(py_list): |
| 634 | + arr[i] = value |
| 635 | + |
| 636 | + return arr |
| 637 | + |
| 638 | + |
| 639 | +def from_c_array(cdata, length, item_type=None): |
| 640 | + """Convert a C array to a Python list. |
| 641 | +
|
| 642 | + Args: |
| 643 | + cdata: CFFI array data |
| 644 | + length: Length of the array |
| 645 | + item_type: Optional type conversion function |
| 646 | +
|
| 647 | + Returns: |
| 648 | + list: Python list with array contents |
| 649 | + """ |
| 650 | + if cdata == ffi.NULL or length <= 0: |
| 651 | + return [] |
| 652 | + |
| 653 | + result = [cdata[i] for i in range(length)] |
| 654 | + |
| 655 | + if item_type: |
| 656 | + result = [item_type(item) for item in result] |
| 657 | + |
| 658 | + return result |
| 659 | + |
| 660 | + |
| 661 | +# Debugging feature: Write the generated C definitions to a file |
| 662 | +# This can be enabled for troubleshooting CFFI binding issues |
345 | 663 | if False: |
346 | 664 | file = open("sciencemode.cdef", "w") |
347 | 665 | file.write(cdef) |
|
0 commit comments