c2py23 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- c2py23/__init__.py +3 -0
- c2py23/c2py_loader.py +126 -0
- c2py23/cli.py +277 -0
- c2py23/generator.py +2003 -0
- c2py23/invariant_checker.py +278 -0
- c2py23/parser.py +1457 -0
- c2py23/perf.py +265 -0
- c2py23/runtime/c2py_amd64.h +49 -0
- c2py23/runtime/c2py_arm64.h +34 -0
- c2py23/runtime/c2py_ppc64.h +27 -0
- c2py23/runtime/c2py_runtime.c +847 -0
- c2py23/runtime/c2py_runtime.h +760 -0
- c2py23-0.3.0.dist-info/METADATA +341 -0
- c2py23-0.3.0.dist-info/RECORD +17 -0
- c2py23-0.3.0.dist-info/WHEEL +5 -0
- c2py23-0.3.0.dist-info/entry_points.txt +2 -0
- c2py23-0.3.0.dist-info/top_level.txt +1 -0
c2py23/__init__.py
ADDED
c2py23/c2py_loader.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""c2py_loader - Load a c2py23-native .so by explicit filename.
|
|
2
|
+
|
|
3
|
+
Convention: <module>.c2py23-<os>_<arch>.so
|
|
4
|
+
|
|
5
|
+
Example files:
|
|
6
|
+
_mymodule.c2py23-linux_x86_64.so
|
|
7
|
+
_mymodule.c2py23-linux_ppc64le.so
|
|
8
|
+
_mymodule.c2py23-linux_aarch64.so
|
|
9
|
+
_mymodule.c2py23-win_amd64.pyd
|
|
10
|
+
_mymodule.c2py23-darwin_arm64.so
|
|
11
|
+
|
|
12
|
+
No monkeypatching of EXTENSION_SUFFIXES. No sys.path hacking.
|
|
13
|
+
Loads the .so by full path via ExtensionFileLoader (Python 3.x) or
|
|
14
|
+
imp.load_dynamic (Python 2.7).
|
|
15
|
+
|
|
16
|
+
Usage in your package's __init__.py:
|
|
17
|
+
|
|
18
|
+
from c2py23.c2py_loader import load_native
|
|
19
|
+
import os as _os
|
|
20
|
+
_mod = load_native(_os.path.dirname(_os.path.abspath(__file__)),
|
|
21
|
+
'_mymodule')
|
|
22
|
+
# Re-export public names (skip dunders, keep single-underscore API)
|
|
23
|
+
for _k, _v in _mod.__dict__.items():
|
|
24
|
+
if _k.startswith('__') and _k.endswith('__'):
|
|
25
|
+
continue
|
|
26
|
+
globals()[_k] = _v
|
|
27
|
+
|
|
28
|
+
Users who do not want a c2py23 runtime dependency can copy the
|
|
29
|
+
function body into their own __init__.py. The code is intentionally
|
|
30
|
+
small and self-contained.
|
|
31
|
+
"""
|
|
32
|
+
from __future__ import print_function
|
|
33
|
+
|
|
34
|
+
import os
|
|
35
|
+
import sys
|
|
36
|
+
import platform as _platform
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _platform_key():
|
|
40
|
+
"""Return 'linux_x86_64', 'win_amd64', 'linux_ppc64le', etc."""
|
|
41
|
+
_os = sys.platform
|
|
42
|
+
if _os == 'linux2':
|
|
43
|
+
_os = 'linux'
|
|
44
|
+
elif _os == 'win32':
|
|
45
|
+
_os = 'win'
|
|
46
|
+
_arch = _platform.machine()
|
|
47
|
+
if _arch == 'AMD64':
|
|
48
|
+
if _os == 'win':
|
|
49
|
+
_arch = 'amd64'
|
|
50
|
+
else:
|
|
51
|
+
_arch = 'x86_64'
|
|
52
|
+
return '%s_%s' % (_os, _arch)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def load_native(package_dir, module_name='_native', tag='c2py23'):
|
|
56
|
+
"""Load <module_name>.<tag>-<platform_key>.so from package_dir.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
package_dir: Absolute path to the package directory containing
|
|
60
|
+
the .so files.
|
|
61
|
+
module_name: Base module name (default '_native').
|
|
62
|
+
Must match the c2py23 YAML 'module:' field.
|
|
63
|
+
Use a unique name per package (e.g. '_mymodule')
|
|
64
|
+
to avoid collisions in sys.modules.
|
|
65
|
+
tag: Tag string inserted in the filename (default 'c2py23').
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The loaded module object. The caller should re-export public
|
|
69
|
+
names from it.
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
_mod = load_native(os.path.dirname(__file__), '_mymodule')
|
|
73
|
+
"""
|
|
74
|
+
_key = _platform_key()
|
|
75
|
+
if os.name == 'nt':
|
|
76
|
+
_ext = '.pyd'
|
|
77
|
+
else:
|
|
78
|
+
_ext = '.so'
|
|
79
|
+
_filename = '%s.%s-%s%s' % (module_name, tag, _key, _ext)
|
|
80
|
+
_path = os.path.join(package_dir, _filename)
|
|
81
|
+
|
|
82
|
+
_trace = os.environ.get('C2PY_TRACE')
|
|
83
|
+
if _trace:
|
|
84
|
+
print('[c2py_loader] platform=%s file=%s' % (_key, _filename),
|
|
85
|
+
file=sys.stderr)
|
|
86
|
+
|
|
87
|
+
if not os.path.isfile(_path):
|
|
88
|
+
_alternatives = [
|
|
89
|
+
f for f in os.listdir(package_dir)
|
|
90
|
+
if f.startswith(module_name + '.')
|
|
91
|
+
]
|
|
92
|
+
_hint = ', '.join(sorted(_alternatives)) if _alternatives else 'none'
|
|
93
|
+
raise ImportError(
|
|
94
|
+
"c2py23: native module not found for platform '%s'\n"
|
|
95
|
+
" Expected: %s\n"
|
|
96
|
+
" Available: %s\n"
|
|
97
|
+
" Build with: gcc -shared -fPIC ... -o %s" %
|
|
98
|
+
(_key, _filename, _hint, _path))
|
|
99
|
+
|
|
100
|
+
if _trace:
|
|
101
|
+
print('[c2py_loader] loading %s -> %s' % (module_name, _path),
|
|
102
|
+
file=sys.stderr)
|
|
103
|
+
|
|
104
|
+
if sys.version_info[0] >= 3:
|
|
105
|
+
import importlib.machinery
|
|
106
|
+
import importlib.util
|
|
107
|
+
loader = importlib.machinery.ExtensionFileLoader(
|
|
108
|
+
module_name, _path)
|
|
109
|
+
spec = importlib.util.spec_from_file_location(
|
|
110
|
+
module_name, _path, loader=loader)
|
|
111
|
+
mod = importlib.util.module_from_spec(spec)
|
|
112
|
+
# Warn if overwriting an existing module in sys.modules.
|
|
113
|
+
# Using distinct module names per package (e.g. '_mymodule'
|
|
114
|
+
# instead of '_native') avoids collisions.
|
|
115
|
+
if module_name in sys.modules:
|
|
116
|
+
import warnings as _w
|
|
117
|
+
_w.warn(
|
|
118
|
+
"c2py_loader: overwriting existing module '%s' "
|
|
119
|
+
"in sys.modules. Use a unique module name."
|
|
120
|
+
% module_name)
|
|
121
|
+
sys.modules[module_name] = mod
|
|
122
|
+
loader.exec_module(mod)
|
|
123
|
+
return mod
|
|
124
|
+
else:
|
|
125
|
+
import imp
|
|
126
|
+
return imp.load_dynamic(module_name, _path)
|
c2py23/cli.py
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""CLI entry point for c2py23.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
c2py23 build foo.c2py [-o foo.so] [--asan] [--generate-only] [--compile-only [--source s.c ...] [--include d/ ...]]
|
|
5
|
+
c2py23 generate foo.c2py [-o wrapper.c]
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import print_function
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
import os
|
|
11
|
+
import subprocess
|
|
12
|
+
import argparse
|
|
13
|
+
|
|
14
|
+
from c2py23.parser import load_c2py
|
|
15
|
+
from c2py23.generator import generate
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _generate_wrapper(c2py_path, output_path=None):
|
|
19
|
+
"""Parse a .c2py file and generate the wrapper C file.
|
|
20
|
+
|
|
21
|
+
Returns (wrapper_path, module_def).
|
|
22
|
+
"""
|
|
23
|
+
if not os.path.exists(c2py_path):
|
|
24
|
+
print("ERROR: file not found: {}".format(c2py_path), file=sys.stderr)
|
|
25
|
+
sys.exit(1)
|
|
26
|
+
|
|
27
|
+
print("Parsing {}...".format(c2py_path))
|
|
28
|
+
module_def = load_c2py(c2py_path)
|
|
29
|
+
mod_name = module_def.name
|
|
30
|
+
|
|
31
|
+
if output_path:
|
|
32
|
+
wrapper_path = output_path
|
|
33
|
+
else:
|
|
34
|
+
wrapper_c = mod_name + '_wrapper.c'
|
|
35
|
+
wrapper_path = os.path.join(os.path.dirname(c2py_path) or '.', wrapper_c)
|
|
36
|
+
|
|
37
|
+
from c2py23.generator import generate as _gen
|
|
38
|
+
c_code = _gen(module_def)
|
|
39
|
+
try:
|
|
40
|
+
with open(wrapper_path, 'w') as f:
|
|
41
|
+
f.write(c_code)
|
|
42
|
+
except IOError as e:
|
|
43
|
+
sys.exit("Error writing {}: {}".format(wrapper_path, e))
|
|
44
|
+
|
|
45
|
+
return wrapper_path, module_def
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _collect_user_sources(base_dir, module_def):
|
|
49
|
+
"""Collect user C source files, resolving relative paths against base_dir.
|
|
50
|
+
|
|
51
|
+
Returns list of absolute paths.
|
|
52
|
+
"""
|
|
53
|
+
source_files = []
|
|
54
|
+
for src in module_def.sources:
|
|
55
|
+
# Normalise: join(base_dir, src) handles both absolute and relative
|
|
56
|
+
src_path = os.path.normpath(os.path.join(base_dir, src))
|
|
57
|
+
if not os.path.exists(src_path):
|
|
58
|
+
print("ERROR: source file not found: {}".format(src_path), file=sys.stderr)
|
|
59
|
+
sys.exit(1)
|
|
60
|
+
source_files.append(src_path)
|
|
61
|
+
return source_files
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _collect_include_dirs(base_dir, module_def, extra_dirs=None):
|
|
65
|
+
"""Collect include directories from module_def sources plus extra dirs.
|
|
66
|
+
|
|
67
|
+
Returns list of unique include directory paths.
|
|
68
|
+
"""
|
|
69
|
+
include_dirs = [base_dir]
|
|
70
|
+
src_dirs = set()
|
|
71
|
+
for src in module_def.sources:
|
|
72
|
+
d = os.path.dirname(os.path.join(base_dir, src))
|
|
73
|
+
if d not in include_dirs:
|
|
74
|
+
src_dirs.add(d)
|
|
75
|
+
for d in sorted(src_dirs):
|
|
76
|
+
include_dirs.append(d)
|
|
77
|
+
if extra_dirs:
|
|
78
|
+
for d in extra_dirs:
|
|
79
|
+
if d not in include_dirs:
|
|
80
|
+
include_dirs.append(d)
|
|
81
|
+
return include_dirs
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _compile_wrapper(wrapper_path, source_files, include_dirs, output_so, asan=False):
|
|
85
|
+
"""Compile a wrapper .c file (plus runtime and user sources) to a .so/.pyd."""
|
|
86
|
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
87
|
+
runtime_dir = os.path.join(script_dir, 'runtime')
|
|
88
|
+
runtime_c = os.path.join(runtime_dir, 'c2py_runtime.c')
|
|
89
|
+
|
|
90
|
+
all_sources = [runtime_c, wrapper_path] + list(source_files)
|
|
91
|
+
for src_path in all_sources:
|
|
92
|
+
if not os.path.exists(src_path):
|
|
93
|
+
print("ERROR: source file not found: {}".format(src_path), file=sys.stderr)
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
|
|
96
|
+
all_includes = [runtime_dir] + list(include_dirs)
|
|
97
|
+
|
|
98
|
+
is_win = sys.platform == 'win32'
|
|
99
|
+
|
|
100
|
+
if is_win:
|
|
101
|
+
cc = os.environ.get('CC', '')
|
|
102
|
+
if not cc:
|
|
103
|
+
cc = _find_msvc() or 'gcc'
|
|
104
|
+
if cc == 'cl' or cc.endswith('cl.exe') or cc.endswith('cl'):
|
|
105
|
+
is_msvc = True
|
|
106
|
+
else:
|
|
107
|
+
is_msvc = False
|
|
108
|
+
else:
|
|
109
|
+
cc = os.environ.get('CC', 'gcc')
|
|
110
|
+
is_msvc = False
|
|
111
|
+
|
|
112
|
+
if is_msvc:
|
|
113
|
+
_default_cflags = '/W4'
|
|
114
|
+
else:
|
|
115
|
+
_default_cflags = '-Wall -Werror -Wpointer-arith'
|
|
116
|
+
cflags = [f for f in os.environ.get('CFLAGS', _default_cflags).split() if f]
|
|
117
|
+
ldflags = [f for f in os.environ.get('LDFLAGS', '').split() if f]
|
|
118
|
+
|
|
119
|
+
if asan:
|
|
120
|
+
if is_msvc:
|
|
121
|
+
cflags.append('/fsanitize=address')
|
|
122
|
+
else:
|
|
123
|
+
cflags.append('-fsanitize=address')
|
|
124
|
+
cflags.append('-g')
|
|
125
|
+
cflags.append('-O1')
|
|
126
|
+
ldflags.append('-fsanitize=address')
|
|
127
|
+
print(" [ASan enabled]")
|
|
128
|
+
|
|
129
|
+
if is_win:
|
|
130
|
+
default_libs = '-lkernel32' if not is_msvc else ''
|
|
131
|
+
libs = os.environ.get('LIBS', default_libs).split()
|
|
132
|
+
libs = [l for l in libs if l]
|
|
133
|
+
else:
|
|
134
|
+
libs = os.environ.get('LIBS', '-ldl -lm').split()
|
|
135
|
+
|
|
136
|
+
if is_msvc:
|
|
137
|
+
include_flags = []
|
|
138
|
+
for d in all_includes:
|
|
139
|
+
include_flags.extend(['/I', d])
|
|
140
|
+
cmd = [cc, '/nologo', '/LD'] + cflags + include_flags + all_sources
|
|
141
|
+
cmd += libs + ['/Fe' + output_so]
|
|
142
|
+
elif is_win:
|
|
143
|
+
include_flags = []
|
|
144
|
+
for d in all_includes:
|
|
145
|
+
include_flags.extend(['-I', d])
|
|
146
|
+
cmd = [cc, '-shared'] + include_flags + cflags + all_sources
|
|
147
|
+
cmd += ldflags + libs + ['-o', output_so]
|
|
148
|
+
else:
|
|
149
|
+
include_flags = []
|
|
150
|
+
for d in all_includes:
|
|
151
|
+
include_flags.extend(['-I', d])
|
|
152
|
+
cmd = ([cc, '-shared', '-fPIC'] + include_flags + cflags +
|
|
153
|
+
all_sources + ldflags + libs + ['-o', output_so])
|
|
154
|
+
|
|
155
|
+
print("Compiling {}...".format(output_so))
|
|
156
|
+
print(" " + ' '.join(cmd))
|
|
157
|
+
ret = subprocess.call(cmd)
|
|
158
|
+
if ret != 0:
|
|
159
|
+
print("ERROR: compilation failed", file=sys.stderr)
|
|
160
|
+
sys.exit(1)
|
|
161
|
+
|
|
162
|
+
print("Success: {}".format(output_so))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _find_msvc():
|
|
166
|
+
"""Find MSVC cl.exe in PATH or standard VS install locations.
|
|
167
|
+
Returns path string or None."""
|
|
168
|
+
import platform as _plat
|
|
169
|
+
for candidate in ['cl', 'cl.exe']:
|
|
170
|
+
for path in os.environ.get('PATH', '').split(os.pathsep):
|
|
171
|
+
full = os.path.join(path, candidate)
|
|
172
|
+
if os.path.isfile(full):
|
|
173
|
+
return candidate
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _determine_so_path(output_arg, default_name, base_dir):
|
|
178
|
+
"""Determine the .so/.pyd output path."""
|
|
179
|
+
if output_arg:
|
|
180
|
+
return output_arg
|
|
181
|
+
ext = '.pyd' if sys.platform == 'win32' else '.so'
|
|
182
|
+
return os.path.join(base_dir, default_name + ext)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def cmd_build(args):
|
|
186
|
+
"""Parse a .c2py file and generate + compile a .so module."""
|
|
187
|
+
c2py_path = args.file
|
|
188
|
+
|
|
189
|
+
# --generate-only: stop after writing wrapper .c
|
|
190
|
+
if getattr(args, 'generate_only', False):
|
|
191
|
+
wrapper_path, _ = _generate_wrapper(c2py_path, args.output)
|
|
192
|
+
print("Wrapper written to: {}".format(wrapper_path))
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
# --compile-only: skip parse+generate, compile existing wrapper.c
|
|
196
|
+
if getattr(args, 'compile_only', False):
|
|
197
|
+
wrapper_path = c2py_path
|
|
198
|
+
if not os.path.exists(wrapper_path):
|
|
199
|
+
print("ERROR: wrapper file not found: {}".format(wrapper_path), file=sys.stderr)
|
|
200
|
+
sys.exit(1)
|
|
201
|
+
|
|
202
|
+
source_files = args.source or []
|
|
203
|
+
source_files = [os.path.abspath(s) for s in source_files]
|
|
204
|
+
|
|
205
|
+
include_dirs = args.include or []
|
|
206
|
+
include_dirs = [os.path.abspath(d) for d in include_dirs]
|
|
207
|
+
|
|
208
|
+
base = os.path.splitext(os.path.basename(wrapper_path))[0]
|
|
209
|
+
so_base = base.replace('_wrapper', '')
|
|
210
|
+
output_so = _determine_so_path(args.output, so_base,
|
|
211
|
+
os.path.dirname(wrapper_path) or '.')
|
|
212
|
+
_compile_wrapper(wrapper_path, source_files, include_dirs, output_so,
|
|
213
|
+
asan=getattr(args, 'asan', False))
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
# Normal build: parse + generate + compile
|
|
217
|
+
base_dir = os.path.dirname(os.path.abspath(c2py_path))
|
|
218
|
+
|
|
219
|
+
wrapper_path, module_def = _generate_wrapper(c2py_path)
|
|
220
|
+
|
|
221
|
+
source_files = _collect_user_sources(base_dir, module_def)
|
|
222
|
+
include_dirs = _collect_include_dirs(base_dir, module_def)
|
|
223
|
+
|
|
224
|
+
output_so = _determine_so_path(args.output, module_def.name, base_dir)
|
|
225
|
+
|
|
226
|
+
_compile_wrapper(wrapper_path, source_files, include_dirs, output_so,
|
|
227
|
+
asan=getattr(args, 'asan', False))
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def cmd_generate(args):
|
|
231
|
+
"""Generate C wrapper from a .c2py file without compiling."""
|
|
232
|
+
wrapper_path, _ = _generate_wrapper(args.file, args.output)
|
|
233
|
+
print("Wrapper written to: %s" % wrapper_path)
|
|
234
|
+
|
|
235
|
+
def _add_build_parser(sub):
|
|
236
|
+
build_p = sub.add_parser('build', help='Build a .so from a .c2py file')
|
|
237
|
+
build_p.add_argument('file', help='Path to .c2py interface file')
|
|
238
|
+
build_p.add_argument('-o', '--output', help='Output .so path (or wrapper .c path with --generate-only)')
|
|
239
|
+
|
|
240
|
+
build_p.add_argument('--asan', action='store_true',
|
|
241
|
+
help='Compile with -fsanitize=address '
|
|
242
|
+
'(detects buffer overflows, leaks, use-after-free)')
|
|
243
|
+
build_p.add_argument('--generate-only', action='store_true',
|
|
244
|
+
help='Generate wrapper .c only, do not compile')
|
|
245
|
+
build_p.add_argument('--compile-only', action='store_true',
|
|
246
|
+
help='Compile an existing wrapper .c file (skip parse+generate)')
|
|
247
|
+
build_p.add_argument('-s', '--source', action='append',
|
|
248
|
+
help='User C source files (repeatable, for --compile-only)')
|
|
249
|
+
build_p.add_argument('-I', '--include', action='append',
|
|
250
|
+
help='Include directories (repeatable, for --compile-only)')
|
|
251
|
+
build_p.set_defaults(func=cmd_build)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _add_generate_parser(sub):
|
|
255
|
+
gen_p = sub.add_parser('generate', help='Generate wrapper .c from .c2py (no compilation)')
|
|
256
|
+
gen_p.add_argument('file', help='Path to .c2py interface file')
|
|
257
|
+
gen_p.add_argument('-o', '--output', help='Output wrapper .c path')
|
|
258
|
+
gen_p.set_defaults(func=cmd_generate)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def main():
|
|
262
|
+
parser = argparse.ArgumentParser(prog='c2py23',
|
|
263
|
+
description='Wrap C99 code to Python via the buffer protocol')
|
|
264
|
+
sub = parser.add_subparsers(dest='command', help='Commands')
|
|
265
|
+
|
|
266
|
+
_add_build_parser(sub)
|
|
267
|
+
_add_generate_parser(sub)
|
|
268
|
+
|
|
269
|
+
args = parser.parse_args()
|
|
270
|
+
if args.command is None:
|
|
271
|
+
parser.print_help()
|
|
272
|
+
sys.exit(1)
|
|
273
|
+
args.func(args)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
if __name__ == '__main__':
|
|
277
|
+
main()
|