Skip to content

Commit e167637

Browse files
feat: improved cffi handling
1 parent 1bd5e2b commit e167637

File tree

6 files changed

+1007
-121
lines changed

6 files changed

+1007
-121
lines changed

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ include lib/*.a
22
include lib/*.lib
33
include sciencemode/*.a
44
include sciencemode/*.lib
5+
include include/*

sciencemode/_cffi.py

Lines changed: 328 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
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+
117
import glob
218
import itertools
319
import os
@@ -10,20 +26,26 @@
1026
from pycparser import c_ast
1127
from pycparser.c_generator import CGenerator
1228

29+
# Regular expressions for parsing include paths and #define statements
1330
INCLUDE_PATTERN = re.compile(r"(-I)?(.*ScienceMode)")
1431
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
1534
DEFINE_BLACKLIST = {
1635
"main",
1736
}
1837

1938
# Try to find the include directory in different locations
39+
# We check multiple possible locations to support different installation structures
2040
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
2749
]
2850

2951
# Find the first valid include directory
@@ -93,6 +115,11 @@
93115
smpt_include_path4 = include_dir
94116

95117
# 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)
96123
DEFINE_ARGS = [
97124
# Platform definitions - set according to current platform
98125
# but make sure to handle platform-specific fields in structures
@@ -134,7 +161,8 @@
134161
"-I" + smpt_include_path4,
135162
]
136163

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
138166

139167
VARIADIC_ARG_PATTERN = re.compile(r"va_list \w+")
140168
ARRAY_SIZEOF_PATTERN = re.compile(r"\[[^\]]*sizeof[^\]]*]")
@@ -205,10 +233,18 @@
205233

206234

207235
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+
208243
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
212248

213249
def process_typedecl(self, node):
214250
coord = os.path.abspath(node.coord.file)
@@ -263,6 +299,123 @@ def visit_FuncDecl(self, node):
263299
ffi = FFI()
264300

265301

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+
266419
ffi.set_source(
267420
"sciencemode._sciencemode",
268421
("\n").join([f'#include "{header}"' for header in ROOT_HEADERS]),
@@ -342,6 +495,171 @@ def visit_FuncDecl(self, node):
342495

343496
ffi.cdef(cdef)
344497

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
345663
if False:
346664
file = open("sciencemode.cdef", "w")
347665
file.write(cdef)

0 commit comments

Comments
 (0)