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 ADDED
@@ -0,0 +1,3 @@
1
+ from __future__ import print_function
2
+
3
+ __version__ = "0.3.0"
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()