diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63935e0..943e556 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,22 @@ jobs: sudo apt-get install -y clang libclang1 - name: Validate repository - run: pixi run check-light + run: pixi run --locked check-light env: # Help libclang find the system shared library on Ubuntu runners. LD_LIBRARY_PATH: /usr/lib/x86_64-linux-gnu + + ffi-runtime: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Pixi + uses: prefix-dev/setup-pixi@v0.8.14 + with: + cache: true + - name: Install C toolchain + run: sudo apt-get update && sudo apt-get install -y clang libclang1 + - name: Verify generated FFI on locked Mojo + run: pixi run --locked pytest -q tests/e2e + env: + LD_LIBRARY_PATH: /usr/lib/x86_64-linux-gnu diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6d63735..884bd76 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,6 +14,7 @@ emits Mojo FFI bindings. Most contribution work falls into one of three areas: The repository currently targets: - Python 3.14+ +- Mojo 1.2+ (1.x), with the exact development version selected by `pixi.lock` - a system `libclang` compatible with the Python `libclang` wheel - Linux for the full Pixi + Mojo workflow @@ -25,7 +26,7 @@ The workspace is currently locked for `linux-64` only. On other platforms, use a normal virtualenv and `pip install -e ".[dev]"` instead. ```bash -pixi install +pixi install --locked pixi shell pixi run lint pixi run format @@ -38,6 +39,8 @@ Useful Pixi tasks: - `pixi run test`: full pytest suite - `pixi run test-light`: pytest excluding `expensive` tests - `pixi run check-light`: CI-aligned validation (`format-check`, lint, pyright, `test-light`) +- `pixi run --locked pytest -q tests/e2e`: the separate FFI runtime CI gate, + including compiler warnings as errors and negative vararg compilation checks - `pixi run build`: build sdist and wheel - `pixi run example-sqlite` / `example-cairo` / `example-libpng` / `example-zlib`: generate and exercise example bindings diff --git a/README.md b/README.md index c526ee4..ae948b8 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ correctly. - Python 3.14+ - a system `libclang` compatible with the `libclang` Python wheel -- a Mojo 1.0+ toolchain if you want to build or run the generated bindings +- a Mojo 1.2+ toolchain (1.x) to build or run generated bindings ## Installation @@ -151,8 +151,9 @@ Current support includes: - **Callbacks and function pointers:** callback typedefs, function-pointer fields, and function-pointer parameters and returns are preserved in Mojo via emitted `comptime` callback declarations and synthesized aliases when needed. -- **Functions:** thin wrappers are generated for non-variadic functions under - both `external_call` and `owned_dl_handle` link modes. +- **Functions:** thin wrappers are generated under both link modes. Dynamic calls + use public, owner-borrowing `OwnedDLHandle` callables. Checked variadic wrappers + are available in `external_call` mode. - **Globals and constants:** because Mojo does not currently expose native C globals directly, supported globals map through generated `GlobalVar` / `GlobalConst` helper structs with synthesized `load()` / `store()` methods; @@ -171,8 +172,9 @@ verify emitted layouts and symbols against your target toolchain. - **Macros:** function-like macros, predefined macros, and more complex preprocessor behavior are preserved but usually emitted as comments for end-user review. -- **Variadics:** variadic C functions are not wrapped as callable thin-FFI - bindings yet and are emitted as comment stubs. +- **Variadics:** direct-call wrappers require exact declared types for the fixed + prefix and explicitly promoted C types for the tail. Dynamic-mode variadics + remain comment stubs because `OwnedDLHandle` has no variadic ABI metadata. - **Non-prototype / K&R-style functions:** older C declaration styles are only partially modeled and should be treated with caution. - **Records with hostile layouts:** some packed, ABI-sensitive, or otherwise @@ -184,7 +186,10 @@ verify emitted layouts and symbols against your target toolchain. record surface. - **Atomics:** atomic support is conservative. Representable atomic fields and pointer-based usage work, but atomic globals are still emitted as stubs and - some surfaces require manual handling. + some surfaces require manual handling. Bindings use `Atomic[T]`; incompatible + atomic storage is opaque and is never silently treated as non-atomic storage. +- **By-value returns:** a mapped return type must be `RegisterPassable`. Returns + containing fixed arrays or opaque storage are emitted as diagnostic stubs. - **Linkage and compiler edge cases:** `inline`, compiler-specific linkage hints, and other extension-heavy cases can still require manual review and may lead to symbol mismatches at runtime. diff --git a/examples/cairo/cairo_smoke.mojo b/examples/cairo/cairo_smoke.mojo index 19ba3dd..202f02c 100644 --- a/examples/cairo/cairo_smoke.mojo +++ b/examples/cairo/cairo_smoke.mojo @@ -1,8 +1,8 @@ # Compact composite smoke test for mojo_bindgen Cairo FFI bindings. # Exercises major Cairo subsystems in grouped checks and one shared image. -from std.ffi import CStringSlice -from std.memory import alloc +from std.ffi import CStringSpan, c_char +from std.memory.alloc import alloc, Layout from cairo_bindings import * comptime OK = CAIRO_STATUS_SUCCESS @@ -48,17 +48,12 @@ def run_metadata_and_lifecycle_checks() raises: _assert("version_positive", cairo_version() > 0) _assert( "version_string_nonnull", - cairo_version_string() - != Optional[Pointer[Int8, ImmUntrackedOrigin]](), + cairo_version_string() != Optional[Pointer[c_char, ImmUntrackedOrigin]](), ) - var surf = cairo_image_surface_create( - materialize[CAIRO_FORMAT_ARGB32](), 96, 96 - ) + var surf = cairo_image_surface_create(materialize[CAIRO_FORMAT_ARGB32](), 96, 96) _ok("base_surface_create", cairo_surface_status(surf)) - var sim = cairo_surface_create_similar( - surf, materialize[CAIRO_CONTENT_COLOR_ALPHA](), 32, 32 - ) + var sim = cairo_surface_create_similar(surf, materialize[CAIRO_CONTENT_COLOR_ALPHA](), 32, 32) _ok("surface_create_similar", cairo_surface_status(sim)) cairo_surface_destroy(sim) cairo_surface_flush(surf) @@ -74,7 +69,7 @@ def run_metadata_and_lifecycle_checks() raises: _assert( "status_to_string_nonnull", cairo_status_to_string(materialize[OK]()) - != Optional[Pointer[Int8, ImmUntrackedOrigin]](), + != Optional[Pointer[c_char, ImmUntrackedOrigin]](), ) cairo_save(cr) @@ -84,9 +79,7 @@ def run_metadata_and_lifecycle_checks() raises: _assert("line_width_restored", _approx_eq(cairo_get_line_width(cr), 2.0)) cairo_set_operator(cr, materialize[CAIRO_OPERATOR_XOR]()) - _assert( - "operator_roundtrip", cairo_get_operator(cr) == CAIRO_OPERATOR_XOR - ) + _assert("operator_roundtrip", cairo_get_operator(cr) == CAIRO_OPERATOR_XOR) cairo_set_operator(cr, materialize[CAIRO_OPERATOR_OVER]()) cairo_set_tolerance(cr, 0.25) _assert("tolerance_roundtrip", _approx_eq(cairo_get_tolerance(cr), 0.25)) @@ -117,13 +110,11 @@ def run_metadata_and_lifecycle_checks() raises: _assert("dash_count_nonneg", cairo_get_dash_count(cr) >= 0) _assert( "get_target_nonnull", - cairo_get_target(cr) - != Optional[Pointer[cairo_surface_t, MutUntrackedOrigin]](), + cairo_get_target(cr) != Optional[Pointer[cairo_surface_t, MutUntrackedOrigin]](), ) _assert( "get_group_target_nonnull", - cairo_get_group_target(cr) - != Optional[Pointer[cairo_surface_t, MutUntrackedOrigin]](), + cairo_get_group_target(cr) != Optional[Pointer[cairo_surface_t, MutUntrackedOrigin]](), ) cairo_destroy(cr) @@ -131,9 +122,7 @@ def run_metadata_and_lifecycle_checks() raises: print("metadata_lifecycle|PASS") -def draw_composite_scene( - cr: Optional[Pointer[cairo_t, MutUntrackedOrigin]] -) raises: +def draw_composite_scene(cr: Optional[Pointer[cairo_t, MutUntrackedOrigin]]) raises: cairo_set_source_rgb(cr, 1.0, 1.0, 1.0) cairo_paint(cr) @@ -202,19 +191,17 @@ def draw_composite_scene( cairo_mesh_pattern_set_corner_color_rgb(mesh, 2, 0.0, 0.0, 1.0) cairo_mesh_pattern_set_corner_color_rgba(mesh, 3, 1.0, 1.0, 0.0, 1.0) cairo_mesh_pattern_end_patch(mesh) - var patch_count = alloc[UInt32](1) + var patch_count = alloc(Layout[UInt32](count=1)).unsafe_leak() _ok( "mesh_patch_count", cairo_mesh_pattern_get_patch_count(mesh, patch_count), ) _assert("mesh_patch_count_ge1", patch_count[] >= 1) - patch_count.free() + patch_count.unsafe_free() cairo_set_source(cr, mesh) cairo_paint_with_alpha(cr, 0.7) - var tiny = cairo_image_surface_create( - materialize[CAIRO_FORMAT_ARGB32](), 8, 8 - ) + var tiny = cairo_image_surface_create(materialize[CAIRO_FORMAT_ARGB32](), 8, 8) var tiny_cr = cairo_create(tiny) cairo_set_source_rgb(tiny_cr, 0.0, 0.0, 0.0) cairo_paint(tiny_cr) @@ -244,15 +231,15 @@ def draw_composite_scene( cairo_fill(cr) var rgba_pat = cairo_pattern_create_rgba(0.8, 0.1, 0.8, 0.6) - var sr = alloc[Float64](1) - var sg = alloc[Float64](1) - var sb = alloc[Float64](1) - var sa = alloc[Float64](1) + var sr = alloc(Layout[Float64](count=1)).unsafe_leak() + var sg = alloc(Layout[Float64](count=1)).unsafe_leak() + var sb = alloc(Layout[Float64](count=1)).unsafe_leak() + var sa = alloc(Layout[Float64](count=1)).unsafe_leak() _ok( "pattern_get_rgba", cairo_pattern_get_rgba(rgba_pat, sr, sg, sb, sa), ) - var matrix = alloc[cairo_matrix_t](1) + var matrix = alloc(Layout[cairo_matrix_t](count=1)).unsafe_leak() cairo_pattern_get_matrix(rgba_pat, matrix) cairo_pattern_set_matrix( rgba_pat, @@ -260,44 +247,44 @@ def draw_composite_scene( Optional[Pointer[cairo_matrix_t, MutUntrackedOrigin]](matrix) ), ) - var surf_ptr = alloc[ - Optional[Pointer[cairo_surface_t, MutUntrackedOrigin]] - ](1) + var surf_ptr = alloc( + Layout[Optional[Pointer[cairo_surface_t, MutUntrackedOrigin]]](count=1) + ).unsafe_leak() _ok("pattern_get_surface", cairo_pattern_get_surface(surface_pat, surf_ptr)) - var x0 = alloc[Float64](1) - var y0 = alloc[Float64](1) - var x1 = alloc[Float64](1) - var y1 = alloc[Float64](1) + var x0 = alloc(Layout[Float64](count=1)).unsafe_leak() + var y0 = alloc(Layout[Float64](count=1)).unsafe_leak() + var x1 = alloc(Layout[Float64](count=1)).unsafe_leak() + var y1 = alloc(Layout[Float64](count=1)).unsafe_leak() _ok( "pattern_get_linear_points", cairo_pattern_get_linear_points(lin, x0, y0, x1, y1), ) - var cx0 = alloc[Float64](1) - var cy0 = alloc[Float64](1) - var r0 = alloc[Float64](1) - var cx1 = alloc[Float64](1) - var cy1 = alloc[Float64](1) - var r1 = alloc[Float64](1) + var cx0 = alloc(Layout[Float64](count=1)).unsafe_leak() + var cy0 = alloc(Layout[Float64](count=1)).unsafe_leak() + var r0 = alloc(Layout[Float64](count=1)).unsafe_leak() + var cx1 = alloc(Layout[Float64](count=1)).unsafe_leak() + var cy1 = alloc(Layout[Float64](count=1)).unsafe_leak() + var r1 = alloc(Layout[Float64](count=1)).unsafe_leak() _ok( "pattern_get_radial_circles", cairo_pattern_get_radial_circles(rad, cx0, cy0, r0, cx1, cy1, r1), ) - sr.free() - sg.free() - sb.free() - sa.free() - matrix.free() - surf_ptr.free() - x0.free() - y0.free() - x1.free() - y1.free() - cx0.free() - cy0.free() - r0.free() - cx1.free() - cy1.free() - r1.free() + sr.unsafe_free() + sg.unsafe_free() + sb.unsafe_free() + sa.unsafe_free() + matrix.unsafe_free() + surf_ptr.unsafe_free() + x0.unsafe_free() + y0.unsafe_free() + x1.unsafe_free() + y1.unsafe_free() + cx0.unsafe_free() + cy0.unsafe_free() + r0.unsafe_free() + cx1.unsafe_free() + cy1.unsafe_free() + r1.unsafe_free() cairo_pattern_destroy(rgba_pat) cairo_pattern_destroy(surface_pat) @@ -352,8 +339,7 @@ def draw_composite_scene( cairo_set_source(cr, source_probe) _assert( "panel4_get_source_nonnull", - cairo_get_source(cr) - != Optional[Pointer[cairo_pattern_t, MutUntrackedOrigin]](), + cairo_get_source(cr) != Optional[Pointer[cairo_pattern_t, MutUntrackedOrigin]](), ) cairo_pattern_destroy(source_probe) cairo_restore(cr) @@ -367,9 +353,7 @@ def draw_composite_scene( _ok("panel5_mask", cairo_status(cr)) cairo_pattern_destroy(mask_pat) - var alpha_surf = cairo_image_surface_create( - materialize[CAIRO_FORMAT_A8](), 280, 160 - ) + var alpha_surf = cairo_image_surface_create(materialize[CAIRO_FORMAT_A8](), 280, 160) var alpha_cr = cairo_create(alpha_surf) cairo_set_source_rgba(alpha_cr, 0.0, 0.0, 0.0, 1.0) cairo_paint(alpha_cr) @@ -383,10 +367,10 @@ def draw_composite_scene( _draw_panel_border(cr, 660.0, 220.0, 280.0, 180.0) cairo_save(cr) cairo_translate(cr, 660.0, 220.0) - var family = CStringSlice("sans\0") + var family = CStringSpan("sans\0") cairo_select_font_face( cr, - rebind[Pointer[Int8, ImmUntrackedOrigin]](family.unsafe_ptr()), + rebind[Pointer[c_char, ImmUntrackedOrigin]](family.ptr()), materialize[CAIRO_FONT_SLANT_NORMAL](), materialize[CAIRO_FONT_WEIGHT_BOLD](), ) @@ -398,13 +382,11 @@ def draw_composite_scene( cairo_font_face_get_type(ff) == CAIRO_FONT_TYPE_TOY, ) var ff2 = cairo_font_face_reference(ff) - _assert( - "panel6_font_refcount_ge2", cairo_font_face_get_reference_count(ff) >= 2 - ) + _assert("panel6_font_refcount_ge2", cairo_font_face_get_reference_count(ff) >= 2) cairo_font_face_destroy(ff2) var tf = cairo_toy_font_face_create( - rebind[Pointer[Int8, ImmUntrackedOrigin]](family.unsafe_ptr()), + rebind[Pointer[c_char, ImmUntrackedOrigin]](family.ptr()), materialize[CAIRO_FONT_SLANT_ITALIC](), materialize[CAIRO_FONT_WEIGHT_NORMAL](), ) @@ -419,22 +401,17 @@ def draw_composite_scene( ) _assert( "panel6_toy_family_nonnull", - cairo_toy_font_face_get_family(tf) - != Optional[Pointer[Int8, ImmUntrackedOrigin]](), + cairo_toy_font_face_get_family(tf) != Optional[Pointer[c_char, ImmUntrackedOrigin]](), ) cairo_font_face_destroy(tf) - var hello = CStringSlice("Hello Cairo\0") + var hello = CStringSpan("Hello Cairo\0") cairo_set_source_rgb(cr, 0.0, 0.0, 0.0) cairo_move_to(cr, 12.0, 60.0) - cairo_show_text( - cr, rebind[Pointer[Int8, ImmUntrackedOrigin]](hello.unsafe_ptr()) - ) + cairo_show_text(cr, rebind[Pointer[c_char, ImmUntrackedOrigin]](hello.ptr())) _ok("panel6_show_text", cairo_status(cr)) cairo_move_to(cr, 12.0, 120.0) - cairo_text_path( - cr, rebind[Pointer[Int8, ImmUntrackedOrigin]](hello.unsafe_ptr()) - ) + cairo_text_path(cr, rebind[Pointer[c_char, ImmUntrackedOrigin]](hello.ptr())) _ok("panel6_text_path", cairo_status(cr)) cairo_new_path(cr) var glyphs = cairo_glyph_allocate(4) @@ -502,62 +479,42 @@ def run_non_visual_object_checks() raises: ) == CAIRO_ANTIALIAS_GRAY, ) - cairo_font_options_set_subpixel_order( - opts, materialize[CAIRO_SUBPIXEL_ORDER_RGB]() - ) + cairo_font_options_set_subpixel_order(opts, materialize[CAIRO_SUBPIXEL_ORDER_RGB]()) _assert( "font_options_subpixel", cairo_font_options_get_subpixel_order( - rebind[ - Optional[ - Pointer[cairo_font_options_t, ImmUntrackedOrigin] - ] - ](opts) + rebind[Optional[Pointer[cairo_font_options_t, ImmUntrackedOrigin]]](opts) ) == CAIRO_SUBPIXEL_ORDER_RGB, ) - cairo_font_options_set_hint_style( - opts, materialize[CAIRO_HINT_STYLE_FULL]() - ) + cairo_font_options_set_hint_style(opts, materialize[CAIRO_HINT_STYLE_FULL]()) _assert( "font_options_hint_style", cairo_font_options_get_hint_style( - rebind[ - Optional[ - Pointer[cairo_font_options_t, ImmUntrackedOrigin] - ] - ](opts) + rebind[Optional[Pointer[cairo_font_options_t, ImmUntrackedOrigin]]](opts) ) == CAIRO_HINT_STYLE_FULL, ) - cairo_font_options_set_hint_metrics( - opts, materialize[CAIRO_HINT_METRICS_ON]() - ) + cairo_font_options_set_hint_metrics(opts, materialize[CAIRO_HINT_METRICS_ON]()) _assert( "font_options_hint_metrics", cairo_font_options_get_hint_metrics( - rebind[ - Optional[ - Pointer[cairo_font_options_t, ImmUntrackedOrigin] - ] - ](opts) + rebind[Optional[Pointer[cairo_font_options_t, ImmUntrackedOrigin]]](opts) ) == CAIRO_HINT_METRICS_ON, ) cairo_font_options_destroy(opts2) cairo_font_options_destroy(opts) - var rect_storage = alloc[cairo_rectangle_int_t](1) - rect_storage[0] = cairo_rectangle_int_t(10, 10, 60, 60) + var rect_storage = alloc(Layout[cairo_rectangle_int_t](count=1)).unsafe_leak() + rect_storage[unsafe_offset=0] = cairo_rectangle_int_t(10, 10, 60, 60) var reg = cairo_region_create_rectangle( - rebind[ - Optional[Pointer[cairo_rectangle_int_t, ImmUntrackedOrigin]] - ](Optional[Pointer[cairo_rectangle_int_t, MutUntrackedOrigin]](rect_storage)) - ) - rect_storage.free() - var reg_const = rebind[ - Optional[Pointer[cairo_region_t, ImmUntrackedOrigin]] - ](reg) + rebind[Optional[Pointer[cairo_rectangle_int_t, ImmUntrackedOrigin]]]( + Optional[Pointer[cairo_rectangle_int_t, MutUntrackedOrigin]](rect_storage) + ) + ) + rect_storage.unsafe_free() + var reg_const = rebind[Optional[Pointer[cairo_region_t, ImmUntrackedOrigin]]](reg) _ok("region_create_rectangle", cairo_region_status(reg_const)) _assert( "region_not_empty", @@ -576,13 +533,11 @@ def run_non_visual_object_checks() raises: cairo_region_num_rectangles(reg_const) >= 0, ) - var extents = alloc[cairo_rectangle_int_t](1) + var extents = alloc(Layout[cairo_rectangle_int_t](count=1)).unsafe_leak() cairo_region_get_extents(reg_const, extents) - extents.free() + extents.unsafe_free() var reg2 = cairo_region_copy(reg_const) - var reg2_const = rebind[ - Optional[Pointer[cairo_region_t, ImmUntrackedOrigin]] - ](reg2) + var reg2_const = rebind[Optional[Pointer[cairo_region_t, ImmUntrackedOrigin]]](reg2) _assert( "region_equal_after_copy", cairo_region_equal(reg_const, reg2_const) != 0, @@ -597,9 +552,7 @@ def run_non_visual_object_checks() raises: cairo_region_destroy(reg_ref) var empty = cairo_region_create() - var empty_const = rebind[ - Optional[Pointer[cairo_region_t, ImmUntrackedOrigin]] - ](empty) + var empty_const = rebind[Optional[Pointer[cairo_region_t, ImmUntrackedOrigin]]](empty) _ok("region_create_empty", cairo_region_status(empty_const)) _assert( "region_empty_is_empty", @@ -613,18 +566,16 @@ def run_non_visual_object_checks() raises: def run_png_roundtrip_from_composite( surface: Optional[Pointer[cairo_surface_t, MutUntrackedOrigin]] ) raises: - var out_path = CStringSlice("/tmp/mojo_bindgen_cairo_smoke_composite.png\0") + var out_path = CStringSpan("/tmp/mojo_bindgen_cairo_smoke_composite.png\0") _ok( "surface_write_to_png", cairo_surface_write_to_png( surface, - rebind[Pointer[Int8, ImmUntrackedOrigin]]( - out_path.unsafe_ptr() - ), + rebind[Pointer[c_char, ImmUntrackedOrigin]](out_path.ptr()), ), ) var loaded = cairo_image_surface_create_from_png( - rebind[Pointer[Int8, ImmUntrackedOrigin]](out_path.unsafe_ptr()) + rebind[Pointer[c_char, ImmUntrackedOrigin]](out_path.ptr()) ) _ok("png_load", cairo_surface_status(loaded)) _assert("png_width", cairo_image_surface_get_width(loaded) == SCENE_W) @@ -642,9 +593,7 @@ def main() raises: print("=== Cairo bindgen compact composite smoke test ===") run_metadata_and_lifecycle_checks() - var surface = cairo_image_surface_create( - materialize[CAIRO_FORMAT_ARGB32](), SCENE_W, SCENE_H - ) + var surface = cairo_image_surface_create(materialize[CAIRO_FORMAT_ARGB32](), SCENE_W, SCENE_H) _ok("composite_surface_create", cairo_surface_status(surface)) var cr = cairo_create(surface) _ok("composite_context_create", cairo_status(cr)) diff --git a/examples/common.sh b/examples/common.sh index 4f1d915..f2328ce 100644 --- a/examples/common.sh +++ b/examples/common.sh @@ -6,8 +6,8 @@ set -euo pipefail example_init() { HERE="$(cd "$(dirname "$1")" && pwd)" REPO_ROOT="$(cd "$HERE/../.." && pwd)" - BG=(pixi run --manifest-path "$REPO_ROOT/pixi.toml" mojo-bindgen) - MOJO=(pixi run --manifest-path "$REPO_ROOT/pixi.toml" mojo) + BG=(pixi run --locked --manifest-path "$REPO_ROOT/pixi.toml" mojo-bindgen) + MOJO=(pixi run --locked --manifest-path "$REPO_ROOT/pixi.toml" mojo) cd "$HERE" } @@ -36,6 +36,14 @@ pkg_header() { local relative="$2" shift 2 + local header_dirs=() + local include_dir + IFS=: read -r -a header_dirs <<< "${CPATH:-}" + for include_dir in "${header_dirs[@]}"; do + [[ -z "$include_dir" ]] && continue + first_existing "$include_dir/$relative" "$include_dir/$(basename "$relative")" && return 0 + done + local flag dir if command -v pkg-config >/dev/null 2>&1; then while read -r -a flags; do @@ -75,6 +83,12 @@ find_dylib() { local link_name="$1" local pkg="${2:-}" local name path flag dir + local token="${link_name//[^a-zA-Z0-9]/_}" + local override="MOJO_BINDGEN_${token^^}_LIBRARY_PATH" + if [[ -n "${!override:-}" ]]; then + echo "${!override}" + return 0 + fi local names=("lib${link_name}.so" "lib${link_name}.so.1" "lib${link_name}.dylib") if [[ -n "$pkg" ]] && command -v pkg-config >/dev/null 2>&1; then @@ -131,5 +145,5 @@ generate_owned_bindings() { build_smoke() { local source="$1" local output="${2:-${source%.mojo}}" - "${MOJO[@]}" build "$source" -I "$HERE" -o "$output" + "${MOJO[@]}" build --Werror "$source" -I "$HERE" -o "$output" } diff --git a/examples/libpng/libpng_smoke.mojo b/examples/libpng/libpng_smoke.mojo index ce3d1a0..593519f 100644 --- a/examples/libpng/libpng_smoke.mojo +++ b/examples/libpng/libpng_smoke.mojo @@ -1,8 +1,8 @@ # Cairo-style composable smoke for external-link libpng bindings. # Requires libpng_bindings.mojo from generate.sh. import libpng_bindings as png -from std.memory import alloc -from std.ffi import c_uint +from std.memory.alloc import alloc, Layout +from std.ffi import c_uint, c_char comptime IMAGE_W = 2 comptime IMAGE_H = 2 @@ -16,12 +16,12 @@ def _assert(label: String, cond: Bool) raises: print(label + "|ok") -def _cstr(s: StaticString) -> Pointer[Int8, ImmUntrackedOrigin]: - return rebind[Pointer[Int8, ImmUntrackedOrigin]](s.unsafe_ptr()) +def _cstr(s: StaticString) -> Pointer[c_char, ImmUntrackedOrigin]: + return rebind[Pointer[c_char, ImmUntrackedOrigin]](s.unsafe_ptr()) -def _cstr_mut(s: StaticString) -> Pointer[Int8, MutUntrackedOrigin]: - return rebind[Pointer[Int8, MutUntrackedOrigin]](s.unsafe_ptr()) +def _cstr_mut(s: StaticString) -> Pointer[c_char, MutUntrackedOrigin]: + return rebind[Pointer[c_char, MutUntrackedOrigin]](s.unsafe_ptr()) def _ignore_png_message(png_ptr: png.png_structp, msg: png.png_const_charp) abi("C"): @@ -39,30 +39,30 @@ def run_version_checks() raises: def run_write_roundtrip_checks() raises: - var pixels = alloc[UInt8](PIXEL_BYTES) + var pixels = alloc(Layout[UInt8](count=PIXEL_BYTES)).unsafe_leak() # BGRA, four pixels, deterministic test pattern. - pixels[0] = 0 - pixels[1] = 0 - pixels[2] = 255 + pixels[unsafe_offset=0] = 0 + pixels[unsafe_offset=1] = 0 + pixels[unsafe_offset=2] = 255 # Make pixel0's alpha 0 so later alpha-removal/compositing checks # are deterministic. - pixels[3] = 0 - pixels[4] = 0 - pixels[5] = 255 - pixels[6] = 0 - pixels[7] = 255 - pixels[8] = 255 - pixels[9] = 0 - pixels[10] = 0 - pixels[11] = 255 - pixels[12] = 255 - pixels[13] = 255 - pixels[14] = 255 - pixels[15] = 255 - - var image_write = alloc[png.png_image](1) - image_write[0] = png.png_image( + pixels[unsafe_offset=3] = 0 + pixels[unsafe_offset=4] = 0 + pixels[unsafe_offset=5] = 255 + pixels[unsafe_offset=6] = 0 + pixels[unsafe_offset=7] = 255 + pixels[unsafe_offset=8] = 255 + pixels[unsafe_offset=9] = 0 + pixels[unsafe_offset=10] = 0 + pixels[unsafe_offset=11] = 255 + pixels[unsafe_offset=12] = 255 + pixels[unsafe_offset=13] = 255 + pixels[unsafe_offset=14] = 255 + pixels[unsafe_offset=15] = 255 + + var image_write = alloc(Layout[png.png_image](count=1)).unsafe_leak() + image_write[unsafe_offset=0] = png.png_image( opaque=Optional[Pointer[png.png_control, MutUntrackedOrigin]](), version=c_uint(png.PNG_IMAGE_VERSION), width=c_uint(IMAGE_W), @@ -71,7 +71,7 @@ def run_write_roundtrip_checks() raises: flags=0, colormap_entries=0, warning_or_error=0, - message=Array[Int8, 64](uninitialized=True), + message=Array[c_char, 64](uninitialized=True), ) var png_path = _cstr("/tmp/mojo_bindgen_libpng_smoke.png") var write_ok = png.png_image_write_to_file( @@ -84,8 +84,8 @@ def run_write_roundtrip_checks() raises: ) _assert("libpng.write_to_file", write_ok != 0) - var image_read = alloc[png.png_image](1) - image_read[0] = png.png_image( + var image_read = alloc(Layout[png.png_image](count=1)).unsafe_leak() + image_read[unsafe_offset=0] = png.png_image( opaque=Optional[Pointer[png.png_control, MutUntrackedOrigin]](), version=c_uint(png.PNG_IMAGE_VERSION), width=0, @@ -94,15 +94,15 @@ def run_write_roundtrip_checks() raises: flags=0, colormap_entries=0, warning_or_error=0, - message=Array[Int8, 64](uninitialized=True), + message=Array[c_char, 64](uninitialized=True), ) var begin_ok = png.png_image_begin_read_from_file(image_read, png_path) _assert("libpng.begin_read_from_file", begin_ok != 0) - _assert("libpng.read_width", image_read[0].width == UInt32(IMAGE_W)) - _assert("libpng.read_height", image_read[0].height == UInt32(IMAGE_H)) + _assert("libpng.read_width", image_read[unsafe_offset=0].width == UInt32(IMAGE_W)) + _assert("libpng.read_height", image_read[unsafe_offset=0].height == UInt32(IMAGE_H)) - image_read[0].format = UInt32(png.PNG_FORMAT_BGRA) - var out_pixels = alloc[UInt8](PIXEL_BYTES) + image_read[unsafe_offset=0].format = UInt32(png.PNG_FORMAT_BGRA) + var out_pixels = alloc(Layout[UInt8](count=PIXEL_BYTES)).unsafe_leak() var finish_ok = png.png_image_finish_read( image_read, png.png_const_colorp(), @@ -111,9 +111,9 @@ def run_write_roundtrip_checks() raises: Optional[OpaquePointer[mut=True, MutUntrackedOrigin]](), ) _assert("libpng.finish_read", finish_ok != 0) - _assert("libpng.pixel_roundtrip_0", out_pixels[0] == pixels[0]) - _assert("libpng.pixel_roundtrip_3", out_pixels[3] == pixels[3]) - _assert("libpng.pixel_roundtrip_15", out_pixels[15] == pixels[15]) + _assert("libpng.pixel_roundtrip_0", out_pixels[unsafe_offset=0] == pixels[unsafe_offset=0]) + _assert("libpng.pixel_roundtrip_3", out_pixels[unsafe_offset=3] == pixels[unsafe_offset=3]) + _assert("libpng.pixel_roundtrip_15", out_pixels[unsafe_offset=15] == pixels[unsafe_offset=15]) png.png_image_free(image_write) png.png_image_free(image_read) @@ -121,29 +121,29 @@ def run_write_roundtrip_checks() raises: def run_memory_roundtrip_checks() raises: - var pixels = alloc[UInt8](PIXEL_BYTES) + var pixels = alloc(Layout[UInt8](count=PIXEL_BYTES)).unsafe_leak() # BGRA, four pixels, deterministic test pattern. - pixels[0] = 0 - pixels[1] = 0 - pixels[2] = 255 - pixels[3] = 0 - pixels[4] = 0 - pixels[5] = 255 - pixels[6] = 0 - pixels[7] = 255 - pixels[8] = 255 - pixels[9] = 0 - pixels[10] = 0 - pixels[11] = 255 - pixels[12] = 255 - pixels[13] = 255 - pixels[14] = 255 - pixels[15] = 255 + pixels[unsafe_offset=0] = 0 + pixels[unsafe_offset=1] = 0 + pixels[unsafe_offset=2] = 255 + pixels[unsafe_offset=3] = 0 + pixels[unsafe_offset=4] = 0 + pixels[unsafe_offset=5] = 255 + pixels[unsafe_offset=6] = 0 + pixels[unsafe_offset=7] = 255 + pixels[unsafe_offset=8] = 255 + pixels[unsafe_offset=9] = 0 + pixels[unsafe_offset=10] = 0 + pixels[unsafe_offset=11] = 255 + pixels[unsafe_offset=12] = 255 + pixels[unsafe_offset=13] = 255 + pixels[unsafe_offset=14] = 255 + pixels[unsafe_offset=15] = 255 # Build a writer-side png_image matching run_write_roundtrip_checks(). - var image_write = alloc[png.png_image](1) - image_write[0] = png.png_image( + var image_write = alloc(Layout[png.png_image](count=1)).unsafe_leak() + image_write[unsafe_offset=0] = png.png_image( opaque=Optional[Pointer[png.png_control, MutUntrackedOrigin]](), version=c_uint(png.PNG_IMAGE_VERSION), width=c_uint(IMAGE_W), @@ -152,12 +152,12 @@ def run_memory_roundtrip_checks() raises: flags=0, colormap_entries=0, warning_or_error=0, - message=Array[Int8, 64](uninitialized=True), + message=Array[c_char, 64](uninitialized=True), ) # Exercise png_image_write_to_memory with both a NULL memory pointer # (size query) and a real memory buffer (actual write). - var memory_bytes_out = alloc[png.png_alloc_size_t](1) + var memory_bytes_out = alloc(Layout[png.png_alloc_size_t](count=1)).unsafe_leak() var write_size_ok = png.png_image_write_to_memory( image_write, Optional[OpaquePointer[mut=True, MutUntrackedOrigin]](), @@ -168,9 +168,9 @@ def run_memory_roundtrip_checks() raises: Optional[OpaquePointer[mut=False, ImmUntrackedOrigin]](), ) _assert("libpng.write_to_memory_size", write_size_ok != 0) - _assert("libpng.write_to_memory_size_nonzero", memory_bytes_out[0] > 0) + _assert("libpng.write_to_memory_size_nonzero", memory_bytes_out[unsafe_offset=0] > 0) - var png_bytes = alloc[UInt8](Int(memory_bytes_out[0])) + var png_bytes = alloc(Layout[UInt8](count=Int(memory_bytes_out[unsafe_offset=0]))).unsafe_leak() var write_ok = png.png_image_write_to_memory( image_write, rebind[OpaquePointer[mut=True, MutUntrackedOrigin]](png_bytes), @@ -182,8 +182,8 @@ def run_memory_roundtrip_checks() raises: ) _assert("libpng.write_to_memory", write_ok != 0) - var image_read = alloc[png.png_image](1) - image_read[0] = png.png_image( + var image_read = alloc(Layout[png.png_image](count=1)).unsafe_leak() + image_read[unsafe_offset=0] = png.png_image( opaque=Optional[Pointer[png.png_control, MutUntrackedOrigin]](), version=UInt32(png.PNG_IMAGE_VERSION), width=0, @@ -192,18 +192,18 @@ def run_memory_roundtrip_checks() raises: flags=0, colormap_entries=0, warning_or_error=0, - message=Array[Int8, 64](uninitialized=True), + message=Array[c_char, 64](uninitialized=True), ) var begin_ok = png.png_image_begin_read_from_memory( image_read, rebind[OpaquePointer[mut=False, ImmUntrackedOrigin]](png_bytes), - UInt(memory_bytes_out[0]), + UInt(memory_bytes_out[unsafe_offset=0]), ) _assert("libpng.begin_read_from_memory", begin_ok != 0) - image_read[0].format = UInt32(png.PNG_FORMAT_BGRA) + image_read[unsafe_offset=0].format = UInt32(png.PNG_FORMAT_BGRA) - var out_pixels = alloc[UInt8](PIXEL_BYTES) + var out_pixels = alloc(Layout[UInt8](count=PIXEL_BYTES)).unsafe_leak() var finish_ok = png.png_image_finish_read( image_read, png.png_const_colorp(), @@ -212,9 +212,11 @@ def run_memory_roundtrip_checks() raises: Optional[OpaquePointer[mut=True, MutUntrackedOrigin]](), ) _assert("libpng.finish_read_memory", finish_ok != 0) - _assert("libpng.pixel_roundtrip_mem_0", out_pixels[0] == pixels[0]) - _assert("libpng.pixel_roundtrip_mem_3", out_pixels[3] == pixels[3]) - _assert("libpng.pixel_roundtrip_mem_15", out_pixels[15] == pixels[15]) + _assert("libpng.pixel_roundtrip_mem_0", out_pixels[unsafe_offset=0] == pixels[unsafe_offset=0]) + _assert("libpng.pixel_roundtrip_mem_3", out_pixels[unsafe_offset=3] == pixels[unsafe_offset=3]) + _assert( + "libpng.pixel_roundtrip_mem_15", out_pixels[unsafe_offset=15] == pixels[unsafe_offset=15] + ) png.png_image_free(image_write) png.png_image_free(image_read) @@ -225,8 +227,8 @@ def run_alpha_removal_compositing_checks() raises: # Re-read the file produced in run_write_roundtrip_checks() and request # RGB output (no alpha). Then confirm compositing with a deterministic # background color. - var image_read = alloc[png.png_image](1) - image_read[0] = png.png_image( + var image_read = alloc(Layout[png.png_image](count=1)).unsafe_leak() + image_read[unsafe_offset=0] = png.png_image( opaque=Optional[Pointer[png.png_control, MutUntrackedOrigin]](), version=UInt32(png.PNG_IMAGE_VERSION), width=0, @@ -235,22 +237,20 @@ def run_alpha_removal_compositing_checks() raises: flags=0, colormap_entries=0, warning_or_error=0, - message=Array[Int8, 64](uninitialized=True), + message=Array[c_char, 64](uninitialized=True), ) var png_path = _cstr("/tmp/mojo_bindgen_libpng_smoke.png") var begin_ok = png.png_image_begin_read_from_file(image_read, png_path) _assert("libpng.alpha_begin_read_from_file", begin_ok != 0) - image_read[0].format = UInt32(png.PNG_FORMAT_RGB) + image_read[unsafe_offset=0].format = UInt32(png.PNG_FORMAT_RGB) - var background = alloc[png.png_color_struct](1) - background[0] = png.png_color_struct(red=7, green=11, blue=13) - var background_ptr = rebind[ - Pointer[png.png_color_struct, ImmUntrackedOrigin] - ](background) + var background = alloc(Layout[png.png_color_struct](count=1)).unsafe_leak() + background[unsafe_offset=0] = png.png_color_struct(red=7, green=11, blue=13) + var background_ptr = rebind[Pointer[png.png_color_struct, ImmUntrackedOrigin]](background) - var out_rgb = alloc[UInt8](IMAGE_W * IMAGE_H * 3) + var out_rgb = alloc(Layout[UInt8](count=IMAGE_W * IMAGE_H * 3)).unsafe_leak() var finish_ok = png.png_image_finish_read( image_read, background_ptr, @@ -261,14 +261,14 @@ def run_alpha_removal_compositing_checks() raises: _assert("libpng.alpha_finish_read", finish_ok != 0) # out_rgb layout for PNG_FORMAT_RGB is R,G,B per pixel (row-major). - _assert("libpng.alpha_removed_pixel0_r", out_rgb[0] == 7) - _assert("libpng.alpha_removed_pixel0_g", out_rgb[1] == 11) - _assert("libpng.alpha_removed_pixel0_b", out_rgb[2] == 13) + _assert("libpng.alpha_removed_pixel0_r", out_rgb[unsafe_offset=0] == 7) + _assert("libpng.alpha_removed_pixel0_g", out_rgb[unsafe_offset=1] == 11) + _assert("libpng.alpha_removed_pixel0_b", out_rgb[unsafe_offset=2] == 13) # Pixel1 is fully opaque in our BGRA test pattern. - _assert("libpng.alpha_removed_pixel1_r", out_rgb[3] == 0) - _assert("libpng.alpha_removed_pixel1_g", out_rgb[4] == 255) - _assert("libpng.alpha_removed_pixel1_b", out_rgb[5] == 0) + _assert("libpng.alpha_removed_pixel1_r", out_rgb[unsafe_offset=3] == 0) + _assert("libpng.alpha_removed_pixel1_g", out_rgb[unsafe_offset=4] == 255) + _assert("libpng.alpha_removed_pixel1_b", out_rgb[unsafe_offset=5] == 0) png.png_image_free(image_read) print("libpng.alpha_removal_composite_ok|", 1) @@ -283,20 +283,14 @@ def run_transform_and_options_checks() raises: ) _assert("libpng.create_write_struct", png_ptr != png.png_structp()) - var info_ptr = png.png_create_info_struct( - rebind[png.png_const_structrp](png_ptr) - ) + var info_ptr = png.png_create_info_struct(rebind[png.png_const_structrp](png_ptr)) _assert("libpng.create_info_struct", info_ptr != png.png_infop()) var png_const_ptr = rebind[png.png_const_structrp](png_ptr) var info_const_ptr = rebind[png.png_const_inforp](info_ptr) # Configure representative writer-side options. - png.png_set_crc_action( - png_ptr, Int32(png.PNG_CRC_WARN_USE), Int32(png.PNG_CRC_WARN_DISCARD) - ) - png.png_set_filter( - png_ptr, Int32(png.PNG_FILTER_TYPE_BASE), Int32(png.PNG_ALL_FILTERS) - ) + png.png_set_crc_action(png_ptr, Int32(png.PNG_CRC_WARN_USE), Int32(png.PNG_CRC_WARN_DISCARD)) + png.png_set_filter(png_ptr, Int32(png.PNG_FILTER_TYPE_BASE), Int32(png.PNG_ALL_FILTERS)) png.png_set_compression_level(png_ptr, 6) png.png_set_compression_strategy(png_ptr, 0) png.png_set_flush(png_ptr, 2) @@ -321,30 +315,32 @@ def run_transform_and_options_checks() raises: Int32(png.PNG_RESOLUTION_METER), ) - var text_entry = alloc[png.png_text_struct](1) - text_entry[0] = png.png_text_struct( + var text_entry = alloc(Layout[png.png_text_struct](count=1)).unsafe_leak() + text_entry[unsafe_offset=0] = png.png_text_struct( compression=Int32(png.PNG_TEXT_COMPRESSION_NONE), key=_cstr_mut("Comment"), text=_cstr_mut("mojo-bindgen-smoke"), text_length=18, itxt_length=0, - lang=Optional[Pointer[Int8, MutUntrackedOrigin]](), - lang_key=Optional[Pointer[Int8, MutUntrackedOrigin]](), + lang=Optional[Pointer[c_char, MutUntrackedOrigin]](), + lang_key=Optional[Pointer[c_char, MutUntrackedOrigin]](), ) png.png_set_text( png_const_ptr, info_ptr, - rebind[png.png_const_textp](Optional[Pointer[png.png_text_struct, MutUntrackedOrigin]](text_entry)), + rebind[png.png_const_textp]( + Optional[Pointer[png.png_text_struct, MutUntrackedOrigin]](text_entry) + ), 1, ) - var get_w = alloc[UInt32](1) - var get_h = alloc[UInt32](1) - var get_bd = alloc[Int32](1) - var get_ct = alloc[Int32](1) - var get_interlace = alloc[Int32](1) - var get_compression = alloc[Int32](1) - var get_filter = alloc[Int32](1) + var get_w = alloc(Layout[UInt32](count=1)).unsafe_leak() + var get_h = alloc(Layout[UInt32](count=1)).unsafe_leak() + var get_bd = alloc(Layout[Int32](count=1)).unsafe_leak() + var get_ct = alloc(Layout[Int32](count=1)).unsafe_leak() + var get_interlace = alloc(Layout[Int32](count=1)).unsafe_leak() + var get_compression = alloc(Layout[Int32](count=1)).unsafe_leak() + var get_filter = alloc(Layout[Int32](count=1)).unsafe_leak() var ihdr_valid = png.png_get_IHDR( png_const_ptr, @@ -358,63 +354,59 @@ def run_transform_and_options_checks() raises: get_filter, ) _assert("libpng.get_ihdr_success", ihdr_valid != 0) - _assert("libpng.ihdr_width", get_w[0] == UInt32(IMAGE_W)) - _assert("libpng.ihdr_height", get_h[0] == UInt32(IMAGE_H)) - _assert("libpng.ihdr_bit_depth", get_bd[0] == 8) + _assert("libpng.ihdr_width", get_w[unsafe_offset=0] == UInt32(IMAGE_W)) + _assert("libpng.ihdr_height", get_h[unsafe_offset=0] == UInt32(IMAGE_H)) + _assert("libpng.ihdr_bit_depth", get_bd[unsafe_offset=0] == 8) _assert( "libpng.ihdr_color_type", - get_ct[0] == Int32(png.PNG_COLOR_TYPE_RGB_ALPHA), + get_ct[unsafe_offset=0] == Int32(png.PNG_COLOR_TYPE_RGB_ALPHA), ) - var res_x = alloc[UInt32](1) - var res_y = alloc[UInt32](1) - var unit_type = alloc[Int32](1) - var phys_valid = png.png_get_pHYs( - png_const_ptr, info_const_ptr, res_x, res_y, unit_type - ) + var res_x = alloc(Layout[UInt32](count=1)).unsafe_leak() + var res_y = alloc(Layout[UInt32](count=1)).unsafe_leak() + var unit_type = alloc(Layout[Int32](count=1)).unsafe_leak() + var phys_valid = png.png_get_pHYs(png_const_ptr, info_const_ptr, res_x, res_y, unit_type) _assert("libpng.get_phys_valid", phys_valid != 0) - _assert("libpng.get_phys_x", res_x[0] == 3780) - _assert("libpng.get_phys_y", res_y[0] == 3780) - _assert("libpng.get_phys_unit", unit_type[0] == Int32(png.PNG_RESOLUTION_METER)) - - var out_text = alloc[png.png_textp](1) - var out_count = alloc[Int32](1) - var text_count = png.png_get_text( - png_const_ptr, info_ptr, out_text, out_count - ) + _assert("libpng.get_phys_x", res_x[unsafe_offset=0] == 3780) + _assert("libpng.get_phys_y", res_y[unsafe_offset=0] == 3780) + _assert("libpng.get_phys_unit", unit_type[unsafe_offset=0] == Int32(png.PNG_RESOLUTION_METER)) + + var out_text = alloc(Layout[png.png_textp](count=1)).unsafe_leak() + var out_count = alloc(Layout[Int32](count=1)).unsafe_leak() + var text_count = png.png_get_text(png_const_ptr, info_ptr, out_text, out_count) _assert("libpng.get_text_count", text_count >= 1) - _assert("libpng.get_text_num", out_count[0] >= 1) + _assert("libpng.get_text_num", out_count[unsafe_offset=0] >= 1) - var png_ptr_ptr = alloc[png.png_structp](1) - var info_ptr_ptr = alloc[png.png_infop](1) - png_ptr_ptr[0] = png_ptr - info_ptr_ptr[0] = info_ptr + var png_ptr_ptr = alloc(Layout[png.png_structp](count=1)).unsafe_leak() + var info_ptr_ptr = alloc(Layout[png.png_infop](count=1)).unsafe_leak() + png_ptr_ptr[unsafe_offset=0] = png_ptr + info_ptr_ptr[unsafe_offset=0] = info_ptr png.png_destroy_write_struct(png_ptr_ptr, info_ptr_ptr) print("libpng.transform_config_ok|", 1) def run_lightweight_struct_api_checks() raises: - var palette = alloc[png.png_color_struct](256) + var palette = alloc(Layout[png.png_color_struct](count=256)).unsafe_leak() png.png_build_grayscale_palette(8, palette) - _assert("libpng.palette_first_black", palette[0].red == 0) - _assert("libpng.palette_last_white", palette[255].red == 255) - - var sig = alloc[UInt8](8) - sig[0] = 137 - sig[1] = 80 - sig[2] = 78 - sig[3] = 71 - sig[4] = 13 - sig[5] = 10 - sig[6] = 26 - sig[7] = 10 + _assert("libpng.palette_first_black", palette[unsafe_offset=0].red == 0) + _assert("libpng.palette_last_white", palette[unsafe_offset=255].red == 255) + + var sig = alloc(Layout[UInt8](count=8)).unsafe_leak() + sig[unsafe_offset=0] = 137 + sig[unsafe_offset=1] = 80 + sig[unsafe_offset=2] = 78 + sig[unsafe_offset=3] = 71 + sig[unsafe_offset=4] = 13 + sig[unsafe_offset=5] = 10 + sig[unsafe_offset=6] = 26 + sig[unsafe_offset=7] = 10 var sig_ok = png.png_sig_cmp( rebind[Pointer[UInt8, ImmUntrackedOrigin]](sig), 0, 8, ) _assert("libpng.sig_cmp_valid", sig_ok == 0) - sig[1] = 81 + sig[unsafe_offset=1] = 81 var sig_bad = png.png_sig_cmp( rebind[Pointer[UInt8, ImmUntrackedOrigin]](sig), 0, diff --git a/examples/sqlite/sqlite_smoke.mojo b/examples/sqlite/sqlite_smoke.mojo index f5e7d93..398440d 100644 --- a/examples/sqlite/sqlite_smoke.mojo +++ b/examples/sqlite/sqlite_smoke.mojo @@ -1,6 +1,6 @@ import sqlite3_bindings as sql -from std.ffi import c_int -from std.memory import alloc +from std.ffi import c_int, c_char +from std.memory.alloc import alloc, Layout # --------------------------- @@ -14,19 +14,15 @@ def _assert(label: String, cond: Bool) raises: print(label + "|ok") -def _cstr(s: StaticString) -> Pointer[Int8, ImmUntrackedOrigin]: - return rebind[Pointer[Int8, ImmUntrackedOrigin]](s.unsafe_ptr()) +def _cstr(s: StaticString) -> Pointer[c_char, ImmUntrackedOrigin]: + return rebind[Pointer[c_char, ImmUntrackedOrigin]](s.unsafe_ptr()) def _ignore_exec_row( data: Optional[OpaquePointer[mut=True, MutUntrackedOrigin]], columns: c_int, - values: Optional[ - Pointer[Optional[Pointer[Int8, MutUntrackedOrigin]], MutUntrackedOrigin] - ], - names: Optional[ - Pointer[Optional[Pointer[Int8, MutUntrackedOrigin]], MutUntrackedOrigin] - ], + values: Optional[Pointer[Optional[Pointer[c_char, MutUntrackedOrigin]], MutUntrackedOrigin]], + names: Optional[Pointer[Optional[Pointer[c_char, MutUntrackedOrigin]], MutUntrackedOrigin]], ) abi("C") -> c_int: return 0 @@ -39,14 +35,12 @@ def _ignore_bound_value( def _errmsg(db: Pointer[sql.sqlite3, MutUntrackedOrigin]) -> String: var p = sql.sqlite3_errmsg(db) - if p == Optional[Pointer[Int8, ImmUntrackedOrigin]](): + if p == Optional[Pointer[c_char, ImmUntrackedOrigin]](): return "" return String(p.value()) -def _check_db_rc( - label: String, db: Pointer[sql.sqlite3, MutUntrackedOrigin], rc: Int32 -) raises: +def _check_db_rc(label: String, db: Pointer[sql.sqlite3, MutUntrackedOrigin], rc: Int32) raises: if rc != Int32(sql.SQLITE_OK): raise Error(label + " rc=" + String(rc) + " err=" + _errmsg(db)) print(label + "|ok") @@ -58,19 +52,21 @@ def _check_db_rc( def _open_memory() raises -> Pointer[sql.sqlite3, MutUntrackedOrigin]: - var ppDb = alloc[Optional[Pointer[sql.sqlite3, MutUntrackedOrigin]]](1) + var ppDb = alloc( + Layout[Optional[Pointer[sql.sqlite3, MutUntrackedOrigin]]](count=1) + ).unsafe_leak() var rc = sql.sqlite3_open_v2( _cstr(":memory:\0"), ppDb, Int32(sql.SQLITE_OPEN_READWRITE | sql.SQLITE_OPEN_CREATE), - Optional[Pointer[Int8, ImmUntrackedOrigin]](), + Optional[Pointer[c_char, ImmUntrackedOrigin]](), ) _assert("open.rc", rc == Int32(sql.SQLITE_OK)) _assert( "open.nonnull", - ppDb[0] != Optional[Pointer[sql.sqlite3, MutUntrackedOrigin]](), + ppDb[unsafe_offset=0] != Optional[Pointer[sql.sqlite3, MutUntrackedOrigin]](), ) - return ppDb[0].value() + return ppDb[unsafe_offset=0].value() def _exec( @@ -83,7 +79,7 @@ def _exec( _cstr(s), _ignore_exec_row, Optional[OpaquePointer[mut=True, MutUntrackedOrigin]](), - alloc[Optional[Pointer[Int8, MutUntrackedOrigin]]](1), + alloc(Layout[Optional[Pointer[c_char, MutUntrackedOrigin]]](count=1)).unsafe_leak(), ) _check_db_rc(label, db, rc) @@ -93,21 +89,21 @@ def _prepare( s: StaticString, label: String, ) raises -> Pointer[sql.sqlite3_stmt, MutUntrackedOrigin]: - var pp = alloc[Optional[Pointer[sql.sqlite3_stmt, MutUntrackedOrigin]]](1) + var pp = alloc( + Layout[Optional[Pointer[sql.sqlite3_stmt, MutUntrackedOrigin]]](count=1) + ).unsafe_leak() var rc = sql.sqlite3_prepare_v2( db, _cstr(s), -1, pp, - alloc[Optional[Pointer[Int8, ImmUntrackedOrigin]]](1), + alloc(Layout[Optional[Pointer[c_char, ImmUntrackedOrigin]]](count=1)).unsafe_leak(), ) _check_db_rc(label, db, rc) - return pp[0].value() + return pp[unsafe_offset=0].value() -def _finalize( - stmt: Pointer[sql.sqlite3_stmt, MutUntrackedOrigin], label: String -) raises: +def _finalize(stmt: Pointer[sql.sqlite3_stmt, MutUntrackedOrigin], label: String) raises: var rc = sql.sqlite3_finalize(stmt) _assert(label, rc == Int32(sql.SQLITE_OK)) @@ -120,7 +116,7 @@ def _finalize( def run_basic_checks() raises: _assert("libversion_number", sql.sqlite3_libversion_number() > 0) var v = sql.sqlite3_libversion() - _assert("libversion_ptr", v != Optional[Pointer[Int8, ImmUntrackedOrigin]]()) + _assert("libversion_ptr", v != Optional[Pointer[c_char, ImmUntrackedOrigin]]()) print("basic|PASS") @@ -201,18 +197,16 @@ def run_text_roundtrip( print("text|PASS") -def run_blob_roundtrip( - db: Pointer[sql.sqlite3, MutUntrackedOrigin] -) raises: +def run_blob_roundtrip(db: Pointer[sql.sqlite3, MutUntrackedOrigin]) raises: _exec(db, "CREATE TABLE b (v BLOB);\0", "blob.schema") var stmt = _prepare(db, "INSERT INTO b(v) VALUES (?);\0", "blob.prepare") - var buf = alloc[UInt8](4) - buf[0] = 1 - buf[1] = 2 - buf[2] = 3 - buf[3] = 4 + var buf = alloc(Layout[UInt8](count=4)).unsafe_leak() + buf[unsafe_offset=0] = 1 + buf[unsafe_offset=1] = 2 + buf[unsafe_offset=2] = 3 + buf[unsafe_offset=3] = 4 var rc = sql.sqlite3_bind_blob( stmt, diff --git a/examples/zlib/zlib_dl_smoke.mojo b/examples/zlib/zlib_dl_smoke.mojo index 1d48022..841ce29 100644 --- a/examples/zlib/zlib_dl_smoke.mojo +++ b/examples/zlib/zlib_dl_smoke.mojo @@ -1,3 +1,5 @@ +from std.ffi import c_char + # Smoke test for owned-dl-handle bindings: dynamic load of libz and runtime FFI calls. from zlib_bindings import ( Z_OK, @@ -11,8 +13,8 @@ from zlib_bindings import ( ) -def _cstr(s: StaticString) -> Pointer[Int8, ImmUntrackedOrigin]: - return rebind[Pointer[Int8, ImmUntrackedOrigin]](s.unsafe_ptr()) +def _cstr(s: StaticString) -> Pointer[c_char, ImmUntrackedOrigin]: + return rebind[Pointer[c_char, ImmUntrackedOrigin]](s.unsafe_ptr()) def main() raises: diff --git a/mojo_bindgen/analysis/common.py b/mojo_bindgen/analysis/common.py index b1683a3..bdcb9a0 100644 --- a/mojo_bindgen/analysis/common.py +++ b/mojo_bindgen/analysis/common.py @@ -8,7 +8,7 @@ """ def struct fn var let inout out mut ref copy owned deinit self Self import from as pass return raise raises try except finally with if elif else for while break continue - and or not is in del alias comptime True False None + and or not is in del alias comptime True False None imm read thin abi match case where std """.split() ) diff --git a/mojo_bindgen/analysis/facts/context.py b/mojo_bindgen/analysis/facts/context.py index 7457245..cd572fe 100644 --- a/mojo_bindgen/analysis/facts/context.py +++ b/mojo_bindgen/analysis/facts/context.py @@ -15,6 +15,8 @@ RecordStorageFacts, analyze_record_storage_facts, ) +from mojo_bindgen.analysis.facts.target import TargetQueries +from mojo_bindgen.analysis.mojo.type_mapping import MapTypePass from mojo_bindgen.ir import Const, Enum, Function, GlobalVar, MacroDecl, Struct, Typedef, Unit @@ -23,6 +25,7 @@ class AnalysisContext: """Reusable facts computed after CIR normalization and before MojoIR mapping.""" unit: Unit + target_queries: TargetQueries records_by_decl_id: dict[str, Struct] typedefs_by_decl_id: dict[str, Typedef] enums_by_decl_id: dict[str, Enum] @@ -40,6 +43,7 @@ def build_analysis_context(unit: Unit) -> AnalysisContext: """Build shared declaration indexes and analysis facts for ``unit``.""" indexes = build_decl_indexes(unit) + target_queries = TargetQueries(unit.target_abi) # First compute C ABI geometry: offsets, sizes, alignment, padding, and # bitfield runs. record_layouts = _build_record_layouts(unit, indexes) @@ -47,9 +51,14 @@ def build_analysis_context(unit: Unit) -> AnalysisContext: alias_classification = classify_aliases(unit) # Then decide Mojo storage eligibility using layout facts plus recursive # record/type context. - record_storage = analyze_record_storage_facts(indexes.records_by_decl_id, record_layouts) + record_storage = analyze_record_storage_facts( + indexes.records_by_decl_id, + record_layouts, + type_mapper=MapTypePass(queries=target_queries), + ) return AnalysisContext( unit=unit, + target_queries=target_queries, records_by_decl_id=indexes.records_by_decl_id, typedefs_by_decl_id=indexes.typedefs_by_decl_id, enums_by_decl_id=indexes.enums_by_decl_id, @@ -71,7 +80,6 @@ def _build_record_layouts( return { decl_id: analyze_record_layout( record, - target_abi=unit.target_abi, record_map=indexes.records_by_decl_id, ) for decl_id, record in indexes.records_by_decl_id.items() diff --git a/mojo_bindgen/analysis/facts/record_layout.py b/mojo_bindgen/analysis/facts/record_layout.py index c6c2730..2b63a08 100644 --- a/mojo_bindgen/analysis/facts/record_layout.py +++ b/mojo_bindgen/analysis/facts/record_layout.py @@ -60,7 +60,7 @@ def is_pure_bitfield(self) -> bool: def analyze_record_layout( decl: Struct, *, - target_abi: TargetABI, + target_abi: TargetABI | None = None, record_map: dict[str, Struct], ) -> RecordLayoutFacts: # Kept in the public signature for callers that pass whole-unit ABI/index diff --git a/mojo_bindgen/analysis/facts/target.py b/mojo_bindgen/analysis/facts/target.py new file mode 100644 index 0000000..52f5228 --- /dev/null +++ b/mojo_bindgen/analysis/facts/target.py @@ -0,0 +1,24 @@ +"""Queries over the canonical target facts attached to a CIR unit.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from mojo_bindgen.ir import TargetABI + + +@dataclass(frozen=True) +class TargetQueries: + """Shared target queries; passes consume answers rather than target fields.""" + + _abi: TargetABI + + def default_char_is_unsigned(self) -> bool | None: + """Return ABI-default char signedness on supported targets, if known. + + Explicit Clang signedness overrides are recorded separately in IntKind. + """ + triple = self._abi.target_triple + if triple is None: + return None + return "linux" in triple and triple.startswith(("aarch64", "arm", "thumb")) diff --git a/mojo_bindgen/analysis/facts/type_layout.py b/mojo_bindgen/analysis/facts/type_layout.py index 1121116..9802b80 100644 --- a/mojo_bindgen/analysis/facts/type_layout.py +++ b/mojo_bindgen/analysis/facts/type_layout.py @@ -23,13 +23,11 @@ def peel_layout_wrappers(t: Type) -> Type: """Strip layout-transparent wrappers from a CIR type.""" - while isinstance(t, (TypeRef, QualifiedType, AtomicType)): + while isinstance(t, (TypeRef, QualifiedType)): if isinstance(t, TypeRef): t = t.canonical elif isinstance(t, QualifiedType): t = t.unqualified - else: # AtomicType - t = t.value_type return t @@ -47,6 +45,9 @@ def type_align(t: Type) -> int | None: def _layout_of(t: Type) -> tuple[int, int] | None: t = peel_layout_wrappers(t) + if isinstance(t, AtomicType): + return _explicit_layout(t.size_bytes, t.align_bytes) or type_layout(t.value_type) + if isinstance( t, ( diff --git a/mojo_bindgen/analysis/mojo/decl_mapping.py b/mojo_bindgen/analysis/mojo/decl_mapping.py index aa88c43..0356786 100644 --- a/mojo_bindgen/analysis/mojo/decl_mapping.py +++ b/mojo_bindgen/analysis/mojo/decl_mapping.py @@ -21,6 +21,7 @@ from mojo_bindgen.ir import ( AliasDecl, AliasKind, + AtomicType, CallExpr, CallTarget, CastExpr, @@ -42,11 +43,11 @@ MojoDecl, NamedType, Param, - ParametricBase, - ParametricType, + QualifiedType, RefExpr, Struct, Typedef, + TypeRef, Unit, ) @@ -153,15 +154,16 @@ def _map_enum(self, decl: Enum) -> list[MojoDecl]: def _map_function(self, decl: Function) -> FunctionDecl: kind = FunctionKind.WRAPPER diagnostics = [] - if decl.is_variadic: - kind = FunctionKind.VARIADIC_STUB - elif decl.attrs.inline_disposition != InlineDisposition.NONE: + if decl.attrs.inline_disposition != InlineDisposition.NONE: kind = FunctionKind.DIRECTIVE_STUB diagnostics.append( stub_note( "inline declaration may not provide a stable external symbol; wrapper omitted" ) ) + elif decl.is_variadic and self.session.options.linking == "owned_dl_handle": + kind = FunctionKind.VARIADIC_STUB + diagnostics.append(stub_note("OwnedDLHandle does not expose variadic C ABI metadata")) return FunctionDecl( name=mojo_ident(decl.name), link_name=decl.link_name, @@ -183,15 +185,21 @@ def _map_function(self, decl: Function) -> FunctionDecl: inline_disposition=decl.attrs.inline_disposition, is_noreturn=decl.attrs.is_noreturn, ), + num_fixed_args=len(decl.params) if decl.is_variadic else None, diagnostics=diagnostics, doc=decl.doc, ) def _map_global(self, decl: GlobalVar) -> GlobalDecl: mapped_type = self.session.type_mapper.run(decl.type) - is_atomic = ( - isinstance(mapped_type, ParametricType) and mapped_type.base == ParametricBase.ATOMIC - ) + source_type = decl.type + while isinstance(source_type, (QualifiedType, TypeRef)): + source_type = ( + source_type.unqualified + if isinstance(source_type, QualifiedType) + else source_type.canonical + ) + is_atomic = isinstance(source_type, AtomicType) return GlobalDecl( name=mojo_ident(decl.name), link_name=decl.link_name, diff --git a/mojo_bindgen/analysis/mojo/mojo_emit_options.py b/mojo_bindgen/analysis/mojo/mojo_emit_options.py index b8e750f..3653262 100644 --- a/mojo_bindgen/analysis/mojo/mojo_emit_options.py +++ b/mojo_bindgen/analysis/mojo/mojo_emit_options.py @@ -23,7 +23,7 @@ class MojoEmitOptions: linking: LinkingMode = "external_call" """external_call: link C symbols at mojo build time; emitted wrappers use ``abi("C")``. - owned_dl_handle: cache a dynamic library handle and resolve explicit C-ABI function pointers; + owned_dl_handle: cache a dynamic library owner and use public C-ABI callables; load or symbol failures abort.""" library_path_hint: str | None = None @@ -42,4 +42,4 @@ class MojoEmitOptions: """When true, preserve parsed C alignment emission behavior. When false, emit ``@align`` only for records with explicit layout intent such as packed or attribute-aligned declarations.""" ffi_origin: FFIOriginStyle = "external" - """Pointer provenance for mapped types: ``external`` → Mut/Immut*ExternalOrigin (recommended for C FFI); ``any`` → *AnyOrigin.""" + """Legacy option; generated C pointers use UntrackedOrigin. Pointer lifetimes remain caller-owned.""" diff --git a/mojo_bindgen/analysis/mojo/record_policies.py b/mojo_bindgen/analysis/mojo/record_policies.py index 16d0968..4217a08 100644 --- a/mojo_bindgen/analysis/mojo/record_policies.py +++ b/mojo_bindgen/analysis/mojo/record_policies.py @@ -10,6 +10,8 @@ Array, BitfieldGroupMember, BuiltinType, + FunctionDecl, + FunctionKind, FunctionPtr, MojoBuiltin, MojoDecl, @@ -82,10 +84,15 @@ def run(self, module: MojoModule) -> MojoModule: self._transfer_cache: dict[str, TransferTraits] = {} self._transfer_computing: set[str] = set() - return replace( - module, - decls=[self._assign_decl_policies(decl) for decl in module.decls], - ) + records = [self._assign_decl_policies(decl) for decl in module.decls] + return replace(module, decls=[self._assign_function_policy(decl) for decl in records]) + + def _assign_function_policy(self, decl: MojoDecl) -> MojoDecl: + if not isinstance(decl, FunctionDecl) or decl.kind != FunctionKind.WRAPPER: + return decl + if self._type_passability(decl.return_type) == MojoPassability.MEMORY_ONLY: + return replace(decl, kind=FunctionKind.NON_REGISTER_RETURN_STUB) + return decl def _assign_decl_policies(self, decl: MojoDecl) -> MojoDecl: if not isinstance(decl, StructDecl): @@ -96,7 +103,7 @@ def _assign_decl_policies(self, decl: MojoDecl) -> MojoDecl: return replace( decl, passability=passability, - traits=[], + traits=[StructTraits.NOT_MOVABLE], fieldwise_init=False, ) @@ -336,6 +343,8 @@ def _traits_for_decl( traits.append(StructTraits.COPYABLE) if transfer_traits.movable: traits.append(StructTraits.MOVABLE) + else: + traits.append(StructTraits.NOT_MOVABLE) return traits @staticmethod diff --git a/mojo_bindgen/analysis/mojo/struct_mapping.py b/mojo_bindgen/analysis/mojo/struct_mapping.py index 726f149..3c09de5 100644 --- a/mojo_bindgen/analysis/mojo/struct_mapping.py +++ b/mojo_bindgen/analysis/mojo/struct_mapping.py @@ -33,7 +33,6 @@ Struct, StructDecl, StructKind, - TargetABI, ) @@ -44,7 +43,6 @@ class StructMappingError(ValueError): @dataclass class StructMappingContext: record_map: dict[str, Struct] - target_abi: TargetABI type_mapper: MapTypePass record_layouts: dict[str, RecordLayoutFacts] = field(default_factory=dict) record_storage: dict[str, RecordStorageFacts] = field(default_factory=dict) @@ -161,12 +159,9 @@ def _map_plain_fields( if isinstance(field.type, AtomicType) and not ( isinstance(mapped_type, ParametricType) and mapped_type.base == ParametricBase.ATOMIC ): - note = ( - "some atomic types were mapped to their underlying non-atomic Mojo type " - "because Atomic[dtype] was not representable" + fallback_reasons.append( + f"atomic field `{display_name}` has no layout-compatible Atomic[T]; opaque storage emitted" ) - if note not in diagnostic_notes: - diagnostic_notes.append(note) plain_fields.append( StoredMember( index=field_fact.index, @@ -268,7 +263,6 @@ def _record_storage( layouts[record.decl_id] = analyze_record_layout( record, record_map=context.record_map, - target_abi=context.target_abi, ) context.record_layouts = layouts context.record_storage = analyze_record_storage_facts( diff --git a/mojo_bindgen/analysis/mojo/type_mapping.py b/mojo_bindgen/analysis/mojo/type_mapping.py index c0a1a95..a06a028 100644 --- a/mojo_bindgen/analysis/mojo/type_mapping.py +++ b/mojo_bindgen/analysis/mojo/type_mapping.py @@ -5,6 +5,7 @@ from dataclasses import dataclass, field from mojo_bindgen.analysis.common import mojo_ident +from mojo_bindgen.analysis.facts.target import TargetQueries from mojo_bindgen.ir import ( _FLOAT_DTYPE_TABLE, _INT_DTYPE_TABLE, @@ -35,6 +36,7 @@ QualifiedType, StructRef, Type, + TypeArg, TypeRef, UnsupportedType, VectorType, @@ -146,6 +148,7 @@ def exact_width_stdint_alias_type(name: str) -> NamedType | None: class MapTypePass: """Map CIR types into surface-oriented MojoIR type nodes.""" + queries: TargetQueries | None = None _cache: dict[int, Type] = field(default_factory=dict, init=False, repr=False) _active: set[int] = field(default_factory=set, init=False, repr=False) # ------------------------ @@ -206,6 +209,12 @@ def _map_primitive(self, t: PrimitiveCIRType) -> Type: if isinstance(t, VoidType): key = "void" elif isinstance(t, IntType): + if t.int_kind in (IntKind.CHAR_S, IntKind.CHAR_U) and self.queries: + unsigned = self.queries.default_char_is_unsigned() + if unsigned is not None: + if (t.int_kind == IntKind.CHAR_U) == unsigned: + return BuiltinType(MojoBuiltin.C_CHAR) + return NamedType("UInt8" if t.int_kind == IntKind.CHAR_U else "Int8") fixed_width_name = _fixed_width_int_name(t) if fixed_width_name == "Int128": return BuiltinType(name=MojoBuiltin.INT128) @@ -231,12 +240,20 @@ def _map_primitive(self, t: PrimitiveCIRType) -> Type: def _map_atomic(self, t: AtomicType) -> Type: dtype = self._dtype_arg(t.value_type) - if dtype is not None: + scalar = self._scalar_for_dtype(t.value_type) + compatible = scalar is not None and ( + (t.size_bytes is None or t.size_bytes == getattr(scalar, "size_bytes", None)) + and (t.align_bytes is None or t.align_bytes == getattr(scalar, "align_bytes", None)) + ) + if dtype is not None and compatible: return ParametricType( base=ParametricBase.ATOMIC, - args=[DTypeArg(dtype)], + args=[TypeArg(self.run(t.value_type))], ) - return self.run(t.value_type) + size = t.size_bytes or getattr(_strip_for_dtype(t.value_type), "size_bytes", None) + if size: + return self._opaque_bytes(size) + raise TypeMappingError("atomic storage has no representable scalar type or known size") def _named(self, name: str) -> NamedType: return NamedType(name=mojo_ident(name.strip())) diff --git a/mojo_bindgen/analysis/mojo/unit_mapping.py b/mojo_bindgen/analysis/mojo/unit_mapping.py index 52048e0..25dbeee 100644 --- a/mojo_bindgen/analysis/mojo/unit_mapping.py +++ b/mojo_bindgen/analysis/mojo/unit_mapping.py @@ -45,7 +45,7 @@ def run(self, unit: Unit) -> MojoModule: context = self._context or build_analysis_context(unit) if context.unit is not unit: context = build_analysis_context(unit) - type_mapper = MapTypePass() + type_mapper = MapTypePass(queries=context.target_queries) session = MappingSession( unit=unit, options=self._options, @@ -55,7 +55,6 @@ def run(self, unit: Unit) -> MojoModule: record_map=context.records_by_decl_id, record_layouts=context.record_layouts, record_storage=context.record_storage, - target_abi=unit.target_abi, type_mapper=type_mapper, ), ) diff --git a/mojo_bindgen/codegen/mojo_ir_printer.py b/mojo_bindgen/codegen/mojo_ir_printer.py index 704f9d6..768af2b 100644 --- a/mojo_bindgen/codegen/mojo_ir_printer.py +++ b/mojo_bindgen/codegen/mojo_ir_printer.py @@ -10,6 +10,7 @@ escape_mojo_string, render_dl_handle_helpers, render_global_symbol_helpers, + render_variadic_helpers, ) from mojo_bindgen.ir import ( AliasDecl, @@ -224,6 +225,7 @@ def _render_module_header(self, module: MojoModule) -> str: return "\n".join( [ "# Generated by mojo_bindgen - do not edit by hand.", + "# Requires Mojo 1.2 or later (1.x).", f"# source: {module.source_header}", f"# library: {module.library} link_name: {module.link_name}", f"# FFI mode: {module.link_mode.value}", @@ -242,6 +244,8 @@ def _render_support_decls(self, module: MojoModule) -> str: chunks.append(render_dl_handle_helpers(module)) elif support.kind == SupportDeclKind.GLOBAL_SYMBOL_HELPERS: chunks.append(render_global_symbol_helpers()) + elif support.kind == SupportDeclKind.VARIADIC_HELPERS: + chunks.append(render_variadic_helpers()) else: raise MojoIRPrintError(f"unsupported SupportDecl kind: {support.kind!r}") return "\n\n".join(chunks) @@ -257,7 +261,6 @@ def _render_decl(self, decl: MojoDecl) -> str: return self._render_global_decl(decl) raise MojoIRPrintError(f"unsupported MojoDecl node: {type(decl).__name__!r}") - # TODO: Revise traits def _render_struct_decl(self, decl: StructDecl) -> str: b = CodeBuilder() b.extend(self._diagnostic_lines(decl.diagnostics)) @@ -267,8 +270,6 @@ def _render_struct_decl(self, decl: StructDecl) -> str: b.add("@fieldwise_init") traits = list(decl.traits) - if decl.kind == StructKind.OPAQUE and not traits: - traits = ["Copyable", "Movable"] trait_text = f"({', '.join(traits)})" if traits else "" b.add(f"struct {decl.name}{trait_text}:") b.indent() @@ -335,7 +336,7 @@ def _render_initializer( f"{self._render_param_name(param.name, i)}: {self._render_type(param.type)}" for i, param in enumerate(initializer.params) ) - b.add(f"def __init__(out self{', ' if params else ''}{params}):") + b.add(f"def __init__(out self: Self{', ' if params else ''}{params}):") b.indent() for storage_name, storage_type in bitfield_storage_types.items(): b.add(f"self.{storage_name} = {storage_type}(0)") @@ -368,7 +369,7 @@ def _render_bitfield_group_accessors( logical_type = self._render_type(bitfield.logical_type) mask_text = self._storage_mask_text(bitfield.bit_width) b.extend(self._doc_comment_lines(bitfield.doc)) - b.add(f"def {bitfield.name}(self) -> {logical_type}:") + b.add(f"def {bitfield.name}(self: Self) -> {logical_type}:") b.indent() self._render_bitfield_accessor_read( b, @@ -379,7 +380,7 @@ def _render_bitfield_group_accessors( mask_text, ) b.dedent() - b.add(f"def set_{bitfield.name}(mut self, value: {logical_type}):") + b.add(f"def set_{bitfield.name}(mut self: Self, value: {logical_type}):") b.indent() self._render_bitfield_accessor_write(b, group, bitfield, storage_type, mask_text) b.dedent() @@ -423,7 +424,7 @@ def _render_bitfield_accessor_read( mask_text: str, ) -> None: little_shift, big_shift = self._bitfield_shifts(group, bitfield) - b.add("comptime if is_big_endian():") + b.add("comptime if Endian.native() == Endian.big:") b.indent() self._render_bitfield_accessor_read_branch( b, @@ -480,7 +481,7 @@ def _render_bitfield_accessor_write( mask_text: str, ) -> None: little_shift, big_shift = self._bitfield_shifts(group, bitfield) - b.add("comptime if is_big_endian():") + b.add("comptime if Endian.native() == Endian.big:") b.indent() self._render_bitfield_accessor_write_branch( b, @@ -572,13 +573,12 @@ def _render_function_decl(self, decl: FunctionDecl) -> str: bracket_inner = ", ".join( [f'"{symbol}"', return_type] + [self._render_type(param.type) for param in decl.params] ) - fn_type = self._render_c_abi_function_pointer_type(decl) call_args = ", ".join(param_names) if decl.kind == FunctionKind.VARIADIC_STUB: b.extend(self._doc_comment_lines(decl.doc)) b.extend(self._diagnostic_lines(decl.diagnostics)) - b.add("# variadic C function - not callable from thin FFI:") + b.add("# variadic C function - OwnedDLHandle does not expose variadic ABI metadata:") b.add(f"# {return_type} {symbol}({params_text}, ...)") return b.render() if decl.kind == FunctionKind.DIRECTIVE_STUB: @@ -598,12 +598,14 @@ def _render_function_decl(self, decl: FunctionDecl) -> str: b.extend(self._doc_comment_lines(decl.doc)) b.extend(self._diagnostic_lines(decl.diagnostics)) b.add( - "# C return type is not RegisterPassable - external_call cannot model this return; bind manually." + "# C return type is not RegisterPassable - Mojo FFI cannot model this return; bind manually." ) b.add(f"# {return_type} {symbol}({params_text})") return b.render() b.extend(self._diagnostic_lines(decl.diagnostics)) + if decl.num_fixed_args is not None: + return self._render_variadic_function(decl, symbol_lit, return_type) if decl.call_target.link_mode == LinkMode.EXTERNAL_CALL: if return_type == "NoneType": b.add(f'def {decl.name}({params_text}) abi("C") -> None:') @@ -617,18 +619,61 @@ def _render_function_decl(self, decl: FunctionDecl) -> str: b.add(f"return external_call[{bracket_inner}]({call_args})") else: fn_local = self._unique_local_name("_bindgen_c_fn", reserved=param_names) - if return_type == "NoneType": - b.add(f"def {decl.name}({params_text}) -> None:") - b.indent() - self._render_docstring(b, decl.doc) - b.add(f'var {fn_local} = _bindgen_function[{fn_type}](StringSpan("{symbol_lit}"))') - b.add(f"{fn_local}({call_args})") - else: - b.add(f"def {decl.name}({params_text}) -> {return_type}:") - b.indent() - self._render_docstring(b, decl.doc) - b.add(f'var {fn_local} = _bindgen_function[{fn_type}](StringSpan("{symbol_lit}"))') - b.add(f"return {fn_local}({call_args})") + owner_local = self._unique_local_name("_bindgen_owner", reserved=param_names) + error_local = self._unique_local_name("_bindgen_error", reserved=param_names) + signature_return = "None" if return_type == "NoneType" else return_type + b.add(f"def {decl.name}({params_text}) -> {signature_return}:") + b.indent() + self._render_docstring(b, decl.doc) + b.add(f"var {owner_local} = _bindgen_dylib()") + b.add("try:") + b.indent() + b.add(f'var {fn_local} = {owner_local}[].get_function[{return_type}]("{symbol_lit}")') + prefix = "" if return_type == "NoneType" else "return " + b.add(f"{prefix}{fn_local}({call_args})") + b.dedent() + b.add(f"except {error_local}:") + b.indent() + b.add( + f"abort(t\"bindgen: failed C function symbol '{symbol_lit}' in dynamic library '{{_BINDGEN_LIB_NAME}}': {{{error_local}}}\")" + ) + b.dedent() + b.dedent() + return b.render() + + def _render_variadic_function(self, decl: FunctionDecl, symbol: str, return_type: str) -> str: + b = CodeBuilder() + b.extend(self._diagnostic_lines(decl.diagnostics)) + signature_return = "None" if return_type == "NoneType" else return_type + b.add( + f"def {decl.name}[*_BindgenArgs: AnyType](*args: *_BindgenArgs) -> {signature_return}:" + ) + b.indent() + self._render_docstring(b, decl.doc) + count = decl.num_fixed_args + b.add( + f'comptime assert _BindgenArgs.length >= {count}, "{decl.name}: missing fixed C arguments"' + ) + # Guard indexing as well as asserting, so invalid arity has a useful diagnostic. + b.add(f"comptime if _BindgenArgs.length >= {count}:") + b.indent() + for i, param in enumerate(decl.params): + type_text = self._render_type(param.type) + b.add( + f'comptime assert _BindgenArgs[{i}] == {type_text}, "{decl.name}: fixed argument {i} must have its declared C type"' + ) + b.add(f"comptime for i in range({count}, _BindgenArgs.length):") + b.indent() + b.add("_bindgen_check_vararg[_BindgenArgs[i]]()") + b.dedent() + prefix = "" if return_type == "NoneType" else "return " + b.add(f'{prefix}external_call["{symbol}", {return_type}, num_fixed_args={count}](*args)') + b.dedent() + if return_type != "NoneType": + b.add("else:") + b.indent() + b.add('abort("bindgen: invalid variadic arity")') + b.dedent() b.dedent() return b.render() @@ -643,11 +688,6 @@ def _render_function_directives(decl: FunctionDecl) -> str: directives.append("noreturn") return ", ".join(directives) - def _render_c_abi_function_pointer_type(self, decl: FunctionDecl) -> str: - params = ", ".join(self._render_type(param.type) for param in decl.params) - return_type = self._render_type(decl.return_type) - return f'def({params}) thin abi("C") -> {return_type}' - def _render_global_decl(self, decl: GlobalDecl) -> str: b = CodeBuilder() b.extend(self._doc_comment_lines(decl.doc)) diff --git a/mojo_bindgen/codegen/mojo_support_templates.py b/mojo_bindgen/codegen/mojo_support_templates.py index 4822bd6..ecee2a2 100644 --- a/mojo_bindgen/codegen/mojo_support_templates.py +++ b/mojo_bindgen/codegen/mojo_support_templates.py @@ -19,181 +19,149 @@ def escape_mojo_string(value: str) -> str: def render_dl_handle_helpers(module: MojoModule) -> str: - library_lit = escape_mojo_string(module.library) - link_lit = escape_mojo_string(module.link_name) + library = escape_mojo_string(module.library) + header = ( + f'comptime _BINDGEN_LIB_NAME = "{library}"\n' + f'comptime _BINDGEN_LINK_NAME = "{escape_mojo_string(module.link_name)}"\n' + ) if module.link_mode == LinkMode.EXTERNAL_CALL: - return _render_external_call_dl_handle_helpers(library_lit, link_lit) - return _render_owned_dl_handle_helpers(module, library_lit, link_lit) + loader = """ +def _bindgen_init_dylib() -> OwnedDLHandle: + try: + return OwnedDLHandle() + except e: + abort(t"bindgen: failed to open process dynamic symbol table: {e}") +""" + else: + header += ( + f'comptime _BINDGEN_LIB_PATH_CANDIDATE: String = "{escape_mojo_string(module.library_path_hint or "")}"\n' + f'comptime _BINDGEN_LIB_PATH_ENV = "{_library_path_env_var(module.link_name)}"\n' + 'comptime _BINDGEN_GENERIC_LIB_PATH_ENV = "MOJO_BINDGEN_LIBRARY_PATH"\n' + ) + loader = _DYNAMIC_LIBRARY_LOADER + return ( + header + + loader + + "\n# Private storage adapter: keep the owner alive for existing free-function bindings.\n" + + f'comptime _BINDGEN_DYLIB = _Global["mojo_bindgen/{library}", _bindgen_init_dylib]\n\n' + + _CACHED_DL_HELPERS + ) def render_global_symbol_helpers() -> str: return _GLOBAL_SYMBOL_HELPERS -def _render_external_call_dl_handle_helpers(library_lit: str, link_lit: str) -> str: - return ( - f'comptime _BINDGEN_LIB_NAME = "{library_lit}"\n' - f'comptime _BINDGEN_LINK_NAME = "{link_lit}"\n' - "\n" - "def _bindgen_init_dylib() -> OwnedDLHandle:\n" - " try:\n" - " return OwnedDLHandle(DEFAULT_RTLD)\n" - " except e:\n" - ' abort(t"bindgen: failed to open process dynamic symbol table: {e}")\n' - "\n" - 'comptime _BINDGEN_DYLIB = _Global["mojo_bindgen/' - f'{library_lit}", _bindgen_init_dylib]\n' - "\n" + _CACHED_DL_HELPERS - ) - - -def _render_owned_dl_handle_helpers( - module: MojoModule, - library_lit: str, - link_lit: str, -) -> str: - path_lit = escape_mojo_string(module.library_path_hint or "") - env_name_lit = escape_mojo_string(_library_path_env_var(module.link_name)) - return ( - f'comptime _BINDGEN_LIB_NAME = "{library_lit}"\n' - f'comptime _BINDGEN_LINK_NAME = "{link_lit}"\n' - f'comptime _BINDGEN_LIB_PATH_CANDIDATE: String = "{path_lit}"\n' - f'comptime _BINDGEN_LIB_PATH_ENV = "{env_name_lit}"\n' - 'comptime _BINDGEN_GENERIC_LIB_PATH_ENV = "MOJO_BINDGEN_LIBRARY_PATH"\n' - "\n" - "def _bindgen_env_path(name: String) -> String:\n" - " var value = getenv(name)\n" - " if value:\n" - " return value\n" - ' return ""\n' - "\n" - "def _bindgen_prefix_lib_path(prefix_name: String, subdir: String) -> String:\n" - " var prefix = getenv(prefix_name)\n" - " if prefix:\n" - ' return prefix + "/" + subdir\n' - ' return ""\n' - "\n" - "def _bindgen_pixi_env_lib_path(subdir: String) -> String:\n" - ' var project_root = getenv("PIXI_PROJECT_ROOT")\n' - ' var env_name = getenv("PIXI_ENVIRONMENT_NAME")\n' - " if project_root and env_name:\n" - ' return project_root + "/.pixi/envs/" + env_name + "/" + subdir\n' - ' return ""\n' - "\n" - "def _bindgen_append_dylib_candidate(mut paths: List[Path], path: String) -> None:\n" - ' if path != "":\n' - " paths.append(Path(path))\n" - "\n" - "def _bindgen_dylib_candidates() -> List[Path]:\n" - " var paths = List[Path]()\n" - " _bindgen_append_dylib_candidate(paths, _bindgen_env_path(_BINDGEN_LIB_PATH_ENV))\n" - " _bindgen_append_dylib_candidate(paths, _bindgen_env_path(_BINDGEN_GENERIC_LIB_PATH_ENV))\n" - " _bindgen_append_dylib_candidate(paths, _BINDGEN_LIB_PATH_CANDIDATE)\n" - ' _bindgen_append_dylib_candidate(paths, _bindgen_prefix_lib_path("CONDA_PREFIX", "lib/lib" + String(_BINDGEN_LINK_NAME) + ".so"))\n' - ' _bindgen_append_dylib_candidate(paths, _bindgen_prefix_lib_path("CONDA_PREFIX", "lib/lib" + String(_BINDGEN_LINK_NAME) + ".so.1"))\n' - ' _bindgen_append_dylib_candidate(paths, _bindgen_prefix_lib_path("CONDA_PREFIX", "lib/lib" + String(_BINDGEN_LINK_NAME) + ".dylib"))\n' - ' _bindgen_append_dylib_candidate(paths, _bindgen_prefix_lib_path("CONDA_PREFIX", "bin/lib" + String(_BINDGEN_LINK_NAME) + ".so"))\n' - ' _bindgen_append_dylib_candidate(paths, _bindgen_prefix_lib_path("CONDA_PREFIX", "bin/lib" + String(_BINDGEN_LINK_NAME) + ".dylib"))\n' - ' _bindgen_append_dylib_candidate(paths, _bindgen_pixi_env_lib_path("lib/lib" + String(_BINDGEN_LINK_NAME) + ".so"))\n' - ' _bindgen_append_dylib_candidate(paths, _bindgen_pixi_env_lib_path("lib/lib" + String(_BINDGEN_LINK_NAME) + ".so.1"))\n' - ' _bindgen_append_dylib_candidate(paths, _bindgen_pixi_env_lib_path("lib/lib" + String(_BINDGEN_LINK_NAME) + ".dylib"))\n' - " _bindgen_append_dylib_candidate(paths, String(_BINDGEN_LINK_NAME))\n" - ' _bindgen_append_dylib_candidate(paths, "lib" + String(_BINDGEN_LINK_NAME) + ".so")\n' - ' _bindgen_append_dylib_candidate(paths, "lib" + String(_BINDGEN_LINK_NAME) + ".so.1")\n' - ' _bindgen_append_dylib_candidate(paths, "lib" + String(_BINDGEN_LINK_NAME) + ".dylib")\n' - ' _bindgen_append_dylib_candidate(paths, "/lib/lib" + String(_BINDGEN_LINK_NAME) + ".so")\n' - ' _bindgen_append_dylib_candidate(paths, "/lib/lib" + String(_BINDGEN_LINK_NAME) + ".so.1")\n' - ' _bindgen_append_dylib_candidate(paths, "/usr/lib/lib" + String(_BINDGEN_LINK_NAME) + ".so")\n' - ' _bindgen_append_dylib_candidate(paths, "/usr/lib/lib" + String(_BINDGEN_LINK_NAME) + ".so.1")\n' - ' _bindgen_append_dylib_candidate(paths, "/usr/local/lib/lib" + String(_BINDGEN_LINK_NAME) + ".so")\n' - ' _bindgen_append_dylib_candidate(paths, "/usr/local/lib/lib" + String(_BINDGEN_LINK_NAME) + ".so.1")\n' - ' _bindgen_append_dylib_candidate(paths, "/lib/x86_64-linux-gnu/lib" + String(_BINDGEN_LINK_NAME) + ".so")\n' - ' _bindgen_append_dylib_candidate(paths, "/lib/x86_64-linux-gnu/lib" + String(_BINDGEN_LINK_NAME) + ".so.1")\n' - ' _bindgen_append_dylib_candidate(paths, "/usr/lib/x86_64-linux-gnu/lib" + String(_BINDGEN_LINK_NAME) + ".so")\n' - ' _bindgen_append_dylib_candidate(paths, "/usr/lib/x86_64-linux-gnu/lib" + String(_BINDGEN_LINK_NAME) + ".so.1")\n' - ' _bindgen_append_dylib_candidate(paths, "/usr/local/lib/lib" + String(_BINDGEN_LINK_NAME) + ".dylib")\n' - ' _bindgen_append_dylib_candidate(paths, "/opt/homebrew/lib/lib" + String(_BINDGEN_LINK_NAME) + ".dylib")\n' - ' _bindgen_append_dylib_candidate(paths, "/usr/lib/lib" + String(_BINDGEN_LINK_NAME) + ".dylib")\n' - " return paths^\n" - "\n" - "def _bindgen_init_dylib() -> OwnedDLHandle:\n" - " var paths = _bindgen_dylib_candidates()\n" - " return _find_dylib[_BINDGEN_LIB_NAME](paths)\n" - "\n" - 'comptime _BINDGEN_DYLIB = _Global["mojo_bindgen/' - f'{library_lit}", _bindgen_init_dylib]\n' - "\n" + _CACHED_DL_HELPERS - ) +def render_variadic_helpers() -> str: + return _VARIADIC_HELPERS def _library_path_env_var(link_name: str) -> str: token = re.sub(r"[^0-9A-Za-z]+", "_", link_name).strip("_").upper() - if not token: - token = "LIBRARY" - return f"MOJO_BINDGEN_{token}_LIBRARY_PATH" - - -_CACHED_DL_HELPERS = """# Returns a borrowed process-lifetime dynamic library handle; do not close it. -def _bindgen_dylib() -> _DLHandle: - var dylib_ptr = _get_global[ - _BINDGEN_DYLIB.name, - _BINDGEN_DYLIB._init_wrapper, - _BINDGEN_DYLIB._deinit_wrapper, - ]() - var dylib = unsafe_cast[Type=_BINDGEN_DYLIB.StorageType](dylib_ptr).value()[].borrow() - if not dylib: - abort(t"bindgen: failed to load dynamic library '{_BINDGEN_LIB_NAME}'") - return dylib - -def _bindgen_function[Fn: TrivialRegisterPassable](symbol: StringSpan) -> Fn: - var fn_ptr = _bindgen_dylib().get_symbol[NoneType](symbol) - if not fn_ptr: - abort( - t"bindgen: missing C function symbol '{symbol}' " - t"in dynamic library '{_BINDGEN_LIB_NAME}'" - ) - return Pointer(to=fn_ptr.value()).unsafe_bitcast[Fn]()[]""" + return f"MOJO_BINDGEN_{token or 'LIBRARY'}_LIBRARY_PATH" + + +_DYNAMIC_LIBRARY_LOADER = """ +def _bindgen_env_path(name: String) -> String: + var value = getenv(name) + if value: + return value + return "" + +def _bindgen_append_dylib_candidate(mut paths: List[Path], path: String): + if path != "": + paths.append(Path(path)) + +def _bindgen_dylib_candidates() -> List[Path]: + var paths = List[Path]() + _bindgen_append_dylib_candidate(paths, _bindgen_env_path(_BINDGEN_LIB_PATH_ENV)) + _bindgen_append_dylib_candidate(paths, _bindgen_env_path(_BINDGEN_GENERIC_LIB_PATH_ENV)) + _bindgen_append_dylib_candidate(paths, _BINDGEN_LIB_PATH_CANDIDATE) + comptime suffix = platform_map["shared library suffix", linux=".so", macos=".dylib"]() + var filename = "lib" + String(_BINDGEN_LINK_NAME) + String(suffix) + var prefix = _bindgen_env_path("CONDA_PREFIX") + if prefix: + paths.append(Path(prefix + "/lib/" + filename)) + paths.append(Path(prefix + "/bin/" + filename)) + var project_root = _bindgen_env_path("PIXI_PROJECT_ROOT") + var env_name = _bindgen_env_path("PIXI_ENVIRONMENT_NAME") + if project_root and env_name: + paths.append(Path(project_root + "/.pixi/envs/" + env_name + "/lib/" + filename)) + paths.append(Path(String(_BINDGEN_LINK_NAME))) + paths.append(Path(filename)) + comptime if CompilationTarget.is_linux(): + paths.append(Path(filename + ".1")) + elif CompilationTarget.is_macos(): + paths.append(Path("/usr/local/lib/" + filename)) + paths.append(Path("/opt/homebrew/lib/" + filename)) + return paths^ + +def _bindgen_init_dylib() -> OwnedDLHandle: + var paths = _bindgen_dylib_candidates() + for path in paths: + try: + return OwnedDLHandle(path) + except: + pass + abort(t"bindgen: failed to load dynamic library '{_BINDGEN_LIB_NAME}' from {paths}") +""" + + +_CACHED_DL_HELPERS = """# The cached owner lives until process teardown; do not close it. +def _bindgen_dylib() -> Pointer[OwnedDLHandle, MutUntrackedOrigin]: + try: + return _BINDGEN_DYLIB.get_or_create_ptr() + except e: + abort(t"bindgen: failed to initialize dynamic library '{_BINDGEN_LIB_NAME}': {e}")""" _GLOBAL_SYMBOL_HELPERS = """struct GlobalVar[T: Copyable & Deinitable, //, link: StaticString]: @staticmethod def _raw() -> Pointer[Self.T, MutUntrackedOrigin]: - var opt: Optional[Pointer[Self.T, MutUntrackedOrigin]] = _bindgen_dylib().get_symbol[Self.T](StringSpan(Self.link)) - if not opt: - abort( - t"bindgen: missing C global symbol '{Self.link}' " - t"in dynamic library '{_BINDGEN_LIB_NAME}'" - ) - return opt.value() + var owner = _bindgen_dylib() + if case .Some(ptr) = owner[].get_symbol[Self.T](StringSpan(Self.link)): + return ptr + abort(t"bindgen: missing C global symbol '{Self.link}' in dynamic library '{_BINDGEN_LIB_NAME}'") @staticmethod def ptr() -> Pointer[Self.T, MutUntrackedOrigin]: - return rebind[Pointer[Self.T, MutUntrackedOrigin]](Self._raw()) + return Self._raw() @staticmethod def load() -> Self.T: return Self._raw()[].copy() @staticmethod - def store(value: Self.T) -> None: - var p = rebind[Pointer[Self.T, MutUntrackedOrigin]](Self._raw()) + def store(value: Self.T): + var p = Self._raw() p[] = value.copy() struct GlobalConst[T: Copyable & Deinitable, //, link: StaticString]: @staticmethod - def _raw() -> Pointer[Self.T, MutUntrackedOrigin]: - var opt: Optional[Pointer[Self.T, MutUntrackedOrigin]] = _bindgen_dylib().get_symbol[Self.T](StringSpan(Self.link)) - if not opt: - abort( - t"bindgen: missing C global symbol '{Self.link}' " - t"in dynamic library '{_BINDGEN_LIB_NAME}'" - ) - return opt.value() + def _raw() -> Pointer[Self.T, ImmUntrackedOrigin]: + var owner = _bindgen_dylib().as_imm() + if case .Some(ptr) = owner[].get_symbol[Self.T](StringSpan(Self.link)): + return ptr + abort(t"bindgen: missing C global symbol '{Self.link}' in dynamic library '{_BINDGEN_LIB_NAME}'") @staticmethod def ptr() -> Pointer[Self.T, ImmUntrackedOrigin]: - return rebind[Pointer[Self.T, ImmUntrackedOrigin]](Self._raw()) + return Self._raw() @staticmethod def load() -> Self.T: return Self._raw()[].copy()""" + + +_VARIADIC_HELPERS = """def _bindgen_check_vararg[T: AnyType](): + # Qualified reflection names identify std pointer families without accepting + # user structs named Pointer/Optional. Restrict pointers to generic address space. + comptime name = reflect[T].name[qualified_builtins=True]() + comptime pointer = name.startswith("std.memory.pointer.Pointer[") and name.endswith(", 0]") + comptime nullable_pointer = name.startswith("std.collections.optional.Optional[std.memory.pointer.Pointer[") and name.endswith(", 0]]") + comptime assert ( + T == c_int or T == c_uint or T == c_long or T == c_ulong + or T == c_long_long or T == c_ulong_long or T == c_double + or ((pointer or nullable_pointer) and conforms_to(T, RegisterPassable)) + ), "C vararg requires a promoted C integer, Float64, or raw pointer; promote narrow integers/Bool to c_int and Float32 to Float64" +""" diff --git a/mojo_bindgen/codegen/normalize_mojo_module.py b/mojo_bindgen/codegen/normalize_mojo_module.py index 30ff026..35b6f95 100644 --- a/mojo_bindgen/codegen/normalize_mojo_module.py +++ b/mojo_bindgen/codegen/normalize_mojo_module.py @@ -370,6 +370,20 @@ def _collect_dependencies(self, decls: list[MojoDecl]) -> ModuleDependencies: for decl in decls: if isinstance(decl, FunctionDecl) and decl.kind == FunctionKind.WRAPPER: + if decl.num_fixed_args is not None: + self._record_support_decl(SupportDeclKind.VARIADIC_HELPERS) + self._record_import("std.reflection", "reflect") + self._record_import("std.os", "abort") + self._record_import( + "std.ffi", + "c_int", + "c_uint", + "c_long", + "c_ulong", + "c_long_long", + "c_ulong_long", + "c_double", + ) if decl.call_target.link_mode == LinkMode.OWNED_DL_HANDLE: self._record_support_decl(SupportDeclKind.DL_HANDLE_HELPERS) elif isinstance(decl, GlobalDecl) and decl.kind == GlobalKind.WRAPPER: @@ -377,17 +391,12 @@ def _collect_dependencies(self, decls: list[MojoDecl]) -> ModuleDependencies: self._record_support_decl(SupportDeclKind.DL_HANDLE_HELPERS) if SupportDeclKind.DL_HANDLE_HELPERS in self._support_decl_kinds: - ffi_imports = ["OwnedDLHandle", "_DLHandle", "_Global", "_get_global"] - if self._module.link_mode == LinkMode.EXTERNAL_CALL: - ffi_imports.append("DEFAULT_RTLD") - else: - ffi_imports.append("_find_dylib") - self._record_import("std.ffi", *ffi_imports) - self._record_import("std.memory.unsafe_pointer", "unsafe_cast") + self._record_import("std.ffi", "OwnedDLHandle", "_Global") os_imports = ["abort"] if self._module.link_mode == LinkMode.OWNED_DL_HANDLE: os_imports.append("getenv") self._record_import("std.pathlib", "Path") + self._record_import("std.sys.info", "CompilationTarget", "platform_map") self._record_import("std.os", *os_imports) for decl in decls: @@ -420,7 +429,7 @@ def _ordered_imports(self) -> list[ModuleImport]: ordered_ffi.append("DEFAULT_RTLD") if "OwnedDLHandle" in ffi_names: ordered_ffi.append("OwnedDLHandle") - for private_name in ("_DLHandle", "_Global", "_find_dylib", "_get_global"): + for private_name in ("_Global",): if private_name in ffi_names: ordered_ffi.append(private_name) if "UnsafeUnion" in ffi_names: @@ -459,6 +468,7 @@ def _ordered_support_decls(self) -> list[SupportDecl]: for kind in ( SupportDeclKind.DL_HANDLE_HELPERS, SupportDeclKind.GLOBAL_SYMBOL_HELPERS, + SupportDeclKind.VARIADIC_HELPERS, ): if kind in self._support_decl_kinds: ordered.append(SupportDecl(kind)) @@ -473,7 +483,7 @@ def _collect_decl_imports( if isinstance(member, StoredMember): self._collect_type_imports(member.type) elif isinstance(member, BitfieldGroupMember): - self._record_import("std.sys.info", "is_big_endian") + self._record_import("std.sys.info", "Endian") self._collect_type_imports(member.storage_type) for field in member.fields: self._collect_type_imports(field.logical_type) @@ -551,7 +561,7 @@ def _collect_type_imports(self, t: Type) -> None: return if isinstance(t, ParametricType): if t.base.value == "SIMD": - self._record_import("std.builtin.simd", "SIMD") + self._record_import("std.simd", "SIMD") elif t.base.value == "ComplexSIMD": self._record_import("std.complex", "ComplexSIMD") elif t.base.value == "Atomic": diff --git a/mojo_bindgen/ir.py b/mojo_bindgen/ir.py index 5917456..216ce7a 100644 --- a/mojo_bindgen/ir.py +++ b/mojo_bindgen/ir.py @@ -208,7 +208,16 @@ class QualifiedType(SerDeMixin): class AtomicType(SerDeMixin): """A C11 ``_Atomic(T)`` wrapper around an underlying value type.""" + SERDE = SerdeSpec( + fields={ + "size_bytes": SerdeFieldSpec(omit_if_default=True), + "align_bytes": SerdeFieldSpec(omit_if_default=True), + } + ) + value_type: Type + size_bytes: int | None = None + align_bytes: int | None = None @dataclass @@ -817,9 +826,12 @@ class IRDiagnostic(SerDeMixin): class TargetABI(SerDeMixin): """Target ABI facts derived from the Clang parse configuration.""" + SERDE = SerdeSpec(fields={"target_triple": SerdeFieldSpec(omit_if_default=True)}) + pointer_size_bytes: int pointer_align_bytes: int byte_order: ByteOrder + target_triple: str | None = None type Decl = Union[ @@ -938,6 +950,7 @@ def decl_from_json(d: dict[str, Any]) -> Decl: class MojoBuiltin(StrEnum): NONE = "NoneType" BOOL = "Bool" + INT8 = "Int8" UINT8 = "UInt8" INT128 = "Int128" UINT128 = "UInt128" @@ -961,7 +974,7 @@ class MojoBuiltin(StrEnum): "void": MojoBuiltin.NONE, IntKind.BOOL: MojoBuiltin.BOOL, IntKind.CHAR_S: MojoBuiltin.C_CHAR, - IntKind.SCHAR: MojoBuiltin.C_CHAR, + IntKind.SCHAR: MojoBuiltin.INT8, IntKind.CHAR_U: MojoBuiltin.C_UCHAR, IntKind.UCHAR: MojoBuiltin.C_UCHAR, IntKind.SHORT: MojoBuiltin.C_SHORT, @@ -1002,6 +1015,7 @@ class MojoBuiltin(StrEnum): _MOJO_INT_TYPES = ( + MojoBuiltin.INT8, MojoBuiltin.C_CHAR, MojoBuiltin.C_UCHAR, MojoBuiltin.C_SHORT, @@ -1064,6 +1078,7 @@ class ModuleImport(SerDeMixin): class SupportDeclKind(StrEnum): DL_HANDLE_HELPERS = "dl_handle_helpers" GLOBAL_SYMBOL_HELPERS = "global_symbol_helpers" + VARIADIC_HELPERS = "variadic_helpers" @dataclass(frozen=True) @@ -1240,6 +1255,7 @@ class FlexibleTail(SerDeMixin): class StructTraits(StrEnum): + NOT_MOVABLE = "not Movable" COPYABLE = "Copyable" IMPLICITLY_COPYABLE = "ImplicitlyCopyable" MOVABLE = "Movable" @@ -1333,6 +1349,7 @@ class FunctionDecl(SerDeMixin): "kind": SerdeFieldSpec(json_key="function_kind"), "attrs": SerdeFieldSpec(omit_if_default=True), "doc": SerdeFieldSpec(omit_if_default=True), + "num_fixed_args": SerdeFieldSpec(omit_if_default=True), } ) @@ -1345,6 +1362,7 @@ class FunctionDecl(SerDeMixin): default_factory=lambda: CallTarget(link_mode=LinkMode.EXTERNAL_CALL, symbol="") ) attrs: FunctionAttrs = field(default_factory=FunctionAttrs) + num_fixed_args: int | None = None diagnostics: list[MappingNote] = field(default_factory=list) doc: DocComment | None = None diff --git a/mojo_bindgen/layout_tests/generator.py b/mojo_bindgen/layout_tests/generator.py index 1e7deac..fcfb0b3 100644 --- a/mojo_bindgen/layout_tests/generator.py +++ b/mojo_bindgen/layout_tests/generator.py @@ -72,7 +72,6 @@ def _layout_record_check( facts = analyze_record_layout( decl, - target_abi=unit.target_abi, record_map=record_map, ) if not facts.is_complete: diff --git a/mojo_bindgen/parsing/lowering/type_lowering.py b/mojo_bindgen/parsing/lowering/type_lowering.py index 527ea06..395e639 100644 --- a/mojo_bindgen/parsing/lowering/type_lowering.py +++ b/mojo_bindgen/parsing/lowering/type_lowering.py @@ -429,9 +429,9 @@ def _lower_atomic(self, t: cx.Type, ctx: TypeContext) -> Type: self.diagnostics.add_type_diag( "warning", t, - "_Atomic lowered to std.Atomic when the value is a representable scalar dtype, otherwise falls back to the underlying non-atomic type", + "_Atomic uses Atomic[T] for layout-compatible scalars; unsupported atomic storage remains opaque", ) - return AtomicType(value_type=inner) + return AtomicType(value_type=inner, size_bytes=_safe_size(t), align_bytes=_safe_align(t)) def _lower_atomic_value_type(self, t: cx.Type, ctx: TypeContext) -> Type: spelling = t.spelling.strip() diff --git a/mojo_bindgen/parsing/target_abi.py b/mojo_bindgen/parsing/target_abi.py index 09598f2..bcfa475 100644 --- a/mojo_bindgen/parsing/target_abi.py +++ b/mojo_bindgen/parsing/target_abi.py @@ -38,6 +38,7 @@ def _extract_pointer_abi(tu: cx.TranslationUnit) -> TargetABI | None: pointer_size_bytes=size_bytes, pointer_align_bytes=align_bytes, byte_order=byte_order, + target_triple=_target_triple_text(target_info), ) return None finally: diff --git a/pixi.lock b/pixi.lock index 6ee85f5..cad3cd5 100644 --- a/pixi.lock +++ b/pixi.lock @@ -192,10 +192,10 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.15-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda - - conda: https://conda.modular.com/max/linux-64/mojo-1.0.0-release.conda - - conda: https://conda.modular.com/max/linux-64/mojo-compiler-1.0.0-release.conda - - conda: https://conda.modular.com/max/noarch/mblack-26.5.0-release.conda - - conda: https://conda.modular.com/max/noarch/mojo-python-1.0.0-release.conda + - conda: https://conda.modular.com/max/linux-64/mojo-1.2.0-release.conda + - conda: https://conda.modular.com/max/linux-64/mojo-compiler-1.2.0-release.conda + - conda: https://conda.modular.com/max/noarch/mblack-26.7.0-release.conda + - conda: https://conda.modular.com/max/noarch/mojo-python-1.2.0-release.conda - pypi: ./ - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1d/fc/716c1e62e512ef1c160e7984a73a5fc7df45166f2ff3f254e71c58076f7c/libclang-18.1.1-py2.py3-none-manylinux2010_x86_64.whl @@ -268,10 +268,10 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda - - conda: https://conda.modular.com/max/linux-64/mojo-1.0.0-release.conda - - conda: https://conda.modular.com/max/linux-64/mojo-compiler-1.0.0-release.conda - - conda: https://conda.modular.com/max/noarch/mblack-26.5.0-release.conda - - conda: https://conda.modular.com/max/noarch/mojo-python-1.0.0-release.conda + - conda: https://conda.modular.com/max/linux-64/mojo-1.2.0-release.conda + - conda: https://conda.modular.com/max/linux-64/mojo-compiler-1.2.0-release.conda + - conda: https://conda.modular.com/max/noarch/mblack-26.7.0-release.conda + - conda: https://conda.modular.com/max/noarch/mojo-python-1.2.0-release.conda - pypi: ./ - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1d/fc/716c1e62e512ef1c160e7984a73a5fc7df45166f2ff3f254e71c58076f7c/libclang-18.1.1-py2.py3-none-manylinux2010_x86_64.whl @@ -2628,53 +2628,53 @@ packages: - pkg:pypi/zipp?source=hash-mapping size: 24190 timestamp: 1779159948016 -- conda: https://conda.modular.com/max/linux-64/mojo-1.0.0-release.conda - sha256: 5778f999b69cf77bd6f07ccaf32d0bafc258fe75206afc8e88919269cebb6e75 - md5: afeb126acb57602a5f9b8153e75e1c3c +- conda: https://conda.modular.com/max/linux-64/mojo-1.2.0-release.conda + sha256: 18485673c7089a187107633a0b9086c5fcb2f0fbe5daf67af44490271e6a2e57 + md5: 24f2a26d0e2cf92bb7ba4712a030a39d depends: - - python >=3.10 - - mojo-compiler ==1.0.0 - - mblack ==26.5.0 + - python >=3.10,<3.15 + - mojo-compiler ==1.2.0 + - mblack ==26.7.0 - jupyter_client >=8.6.2,<8.7 license: LicenseRef-Modular-Proprietary run_exports: {} - size: 115641078 - timestamp: 1786292404342 -- conda: https://conda.modular.com/max/linux-64/mojo-compiler-1.0.0-release.conda - sha256: 4394c6146d47ec7794a9a3ed5775ae158f59f83f8e1aed59408b17c4909821b3 - md5: 75d2e2ca9d87fdc76513d7939202e7dc + size: 113694280 + timestamp: 1792188456301 +- conda: https://conda.modular.com/max/linux-64/mojo-compiler-1.2.0-release.conda + sha256: a746d7db173a9882e7073ae6d905acfe383a754faecb4a53710b7b90db377e6b + md5: 7029fcd79f3dc5573e9c73eae92b37e0 depends: - - mojo-python ==1.0.0 + - mojo-python ==1.2.0 license: LicenseRef-Modular-Proprietary run_exports: {} - size: 68511359 - timestamp: 1786292410712 -- conda: https://conda.modular.com/max/noarch/mblack-26.5.0-release.conda + size: 71258238 + timestamp: 1792188428581 +- conda: https://conda.modular.com/max/noarch/mblack-26.7.0-release.conda noarch: python - sha256: 49102b55366eab56b04620533302b8693b4fa4a6c5972b3cc978b3f3bad83ebe - md5: 0cbfc119215c063cd279f0a6beb04285 + sha256: 4f2478dfce255de9825609cc199b84920e78bdc631624bb96f132ddd0abe766c + md5: 5759078a3ebbe08e3339a60ae876a02f depends: - - python >=3.10 + - python >=3.10,<3.15 - click >=8.0.0 - mypy_extensions >=0.4.3 - packaging >=22.0 - - pathspec >=0.9.0 + - pathspec >=1.1.0 - platformdirs >=2 - tomli >=1.1.0 license: LicenseRef-Modular-Proprietary run_exports: {} - size: 137187 - timestamp: 1786151006088 -- conda: https://conda.modular.com/max/noarch/mojo-python-1.0.0-release.conda + size: 141260 + timestamp: 1792101242288 +- conda: https://conda.modular.com/max/noarch/mojo-python-1.2.0-release.conda noarch: python - sha256: fc449a47c7707ed96a794768e29d5d2b0bf7afab373e2a9751f281ed7c4f822c - md5: 4a27e1596a5c2d27330a2377090b12c5 + sha256: 52905cdbe15595db926615438dd3bc8a74a5277e55c4e0821ccf45ee3719cdb7 + md5: f5bca66e7717194164db37455fd5ed2d depends: - - python >=3.10 + - python >=3.10,<3.15 license: LicenseRef-Modular-Proprietary run_exports: {} - size: 24040 - timestamp: 1785891558838 + size: 26506 + timestamp: 1792101239805 - pypi: ./ name: mojo-bindgen requires_dist: diff --git a/pixi.toml b/pixi.toml index 1a89172..e29ac37 100644 --- a/pixi.toml +++ b/pixi.toml @@ -28,7 +28,7 @@ example-libpng = "bash examples/libpng/generate.sh" example-sqlite = "bash examples/sqlite/generate.sh" [dependencies] -mojo = "1.*" +mojo = ">=1.2,<2" python = ">=3.14.4,<3.15" pip = ">=26.0.1,<27" rich = ">=15.0.0,<16" diff --git a/tests/e2e/README.md b/tests/e2e/README.md index 9674724..265d337 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -21,12 +21,12 @@ pixi run pytest -q -m "not expensive" Run only the new golden runtime suite: ```bash -pixi run pytest tests/e2e/test_golden_runtime.py -v -s +pixi run pytest tests/e2e/test_e2e_runtime.py -v -s ``` ## Golden Runtime Corpus -`tests/e2e/golden` is a runtime-first golden corpus. Every case must include: +`tests/e2e/fixtures` is a runtime-first golden corpus. Every case must include: - `input.h` - `impl.c` @@ -55,9 +55,10 @@ The test runner reports: ## Existing Coverage -- `test_runtime_ffi.py`: legacy fixture-based runtime checks. - parser/emit surface checks now live under `tests/unit/` and `tests/stress/`. -- `test_golden_runtime.py`: comprehensive golden runtime suite with explicit failure attribution. +- `test_e2e_runtime.py`: comprehensive golden runtime suite with explicit failure attribution. +- `test_mojo_12.py`: checked varargs, over-aligned arrays, dynamic globals, loader + override precedence, and failure diagnostics. Both runtime suites compile with `--Werror`. ## Golden Case Matrix diff --git a/tests/e2e/fixtures/atomic_types_runtime/expect.emit.external.mojo b/tests/e2e/fixtures/atomic_types_runtime/expect.emit.external.mojo index 849889f..4080dd6 100644 --- a/tests/e2e/fixtures/atomic_types_runtime/expect.emit.external.mojo +++ b/tests/e2e/fixtures/atomic_types_runtime/expect.emit.external.mojo @@ -1,11 +1,11 @@ # Each non-comment line must be present in the emitted file. -# global variable at_counter: Atomic[DType.int32] (atomic global requires manual binding (use Atomic APIs on a pointer)) +# global variable at_counter: Atomic[c_int] (atomic global requires manual binding (use Atomic APIs on a pointer)) def at_reset( def at_inc( def at_dec( def at_get( -def at_addr() abi("C") -> Optional[Pointer[Atomic[DType.int32], MutUntrackedOrigin]]: -def at_addr_const() abi("C") -> Optional[Pointer[Atomic[DType.int32], ImmUntrackedOrigin]]: -def at_store_ptr(dst: Optional[Pointer[Atomic[DType.int32], MutUntrackedOrigin]], value: c_int) abi("C") -> None: -def at_load_ptr(src: Optional[Pointer[Atomic[DType.int32], ImmUntrackedOrigin]]) abi("C") -> c_int: -def at_inc_ptr(dst: Optional[Pointer[Atomic[DType.int32], MutUntrackedOrigin]]) abi("C") -> c_int: +def at_addr() abi("C") -> Optional[Pointer[Atomic[c_int], MutUntrackedOrigin]]: +def at_addr_const() abi("C") -> Optional[Pointer[Atomic[c_int], ImmUntrackedOrigin]]: +def at_store_ptr(dst: Optional[Pointer[Atomic[c_int], MutUntrackedOrigin]], value: c_int) abi("C") -> None: +def at_load_ptr(src: Optional[Pointer[Atomic[c_int], ImmUntrackedOrigin]]) abi("C") -> c_int: +def at_inc_ptr(dst: Optional[Pointer[Atomic[c_int], MutUntrackedOrigin]]) abi("C") -> c_int: diff --git a/tests/e2e/fixtures/atomic_types_runtime/expect.emit.owned_dl_handle.mojo b/tests/e2e/fixtures/atomic_types_runtime/expect.emit.owned_dl_handle.mojo index db473ab..50b2434 100644 --- a/tests/e2e/fixtures/atomic_types_runtime/expect.emit.owned_dl_handle.mojo +++ b/tests/e2e/fixtures/atomic_types_runtime/expect.emit.owned_dl_handle.mojo @@ -1,11 +1,11 @@ # Each non-comment line must be present in the emitted file. -# global variable at_counter: Atomic[DType.int32] (atomic global requires manual binding (use Atomic APIs on a pointer)) +# global variable at_counter: Atomic[c_int] (atomic global requires manual binding (use Atomic APIs on a pointer)) def at_reset( def at_inc( def at_dec( def at_get( -def at_addr() -> Optional[Pointer[Atomic[DType.int32], MutUntrackedOrigin]]: -def at_addr_const() -> Optional[Pointer[Atomic[DType.int32], ImmUntrackedOrigin]]: -def at_store_ptr(dst: Optional[Pointer[Atomic[DType.int32], MutUntrackedOrigin]], value: c_int) -> None: -def at_load_ptr(src: Optional[Pointer[Atomic[DType.int32], ImmUntrackedOrigin]]) -> c_int: -def at_inc_ptr(dst: Optional[Pointer[Atomic[DType.int32], MutUntrackedOrigin]]) -> c_int: +def at_addr() -> Optional[Pointer[Atomic[c_int], MutUntrackedOrigin]]: +def at_addr_const() -> Optional[Pointer[Atomic[c_int], ImmUntrackedOrigin]]: +def at_store_ptr(dst: Optional[Pointer[Atomic[c_int], MutUntrackedOrigin]], value: c_int) -> None: +def at_load_ptr(src: Optional[Pointer[Atomic[c_int], ImmUntrackedOrigin]]) -> c_int: +def at_inc_ptr(dst: Optional[Pointer[Atomic[c_int], MutUntrackedOrigin]]) -> c_int: diff --git a/tests/e2e/fixtures/fnptr_return/expect.emit.owned_dl_handle.mojo b/tests/e2e/fixtures/fnptr_return/expect.emit.owned_dl_handle.mojo index c72eaa8..02fc943 100644 --- a/tests/e2e/fixtures/fnptr_return/expect.emit.owned_dl_handle.mojo +++ b/tests/e2e/fixtures/fnptr_return/expect.emit.owned_dl_handle.mojo @@ -1,5 +1,5 @@ comptime int32_t = Int32 comptime pfr_binary_op_t = def (arg0: int32_t, arg1: int32_t) thin abi("C") -> int32_t def pfr_select_add() -> pfr_binary_op_t: -_bindgen_function[def() thin abi("C") -> pfr_binary_op_t](StringSpan("pfr_select_add")) +.get_function[pfr_binary_op_t]("pfr_select_add") def pfr_sanity( diff --git a/tests/e2e/fixtures/full_surface_runtime/expect.emit.owned_dl_handle.mojo b/tests/e2e/fixtures/full_surface_runtime/expect.emit.owned_dl_handle.mojo index 5408558..0d60dba 100644 --- a/tests/e2e/fixtures/full_surface_runtime/expect.emit.owned_dl_handle.mojo +++ b/tests/e2e/fixtures/full_surface_runtime/expect.emit.owned_dl_handle.mojo @@ -1,5 +1,5 @@ def _bindgen_dylib( -_bindgen_function[ +.get_function[ struct surf_point struct surf_pair struct surf_flags diff --git a/tests/e2e/fixtures/functional_math/expect.emit.owned_dl_handle.mojo b/tests/e2e/fixtures/functional_math/expect.emit.owned_dl_handle.mojo index d152449..402c020 100644 --- a/tests/e2e/fixtures/functional_math/expect.emit.owned_dl_handle.mojo +++ b/tests/e2e/fixtures/functional_math/expect.emit.owned_dl_handle.mojo @@ -1,6 +1,6 @@ # Each non-comment line must be present in the emitted file. def _bindgen_dylib( -_bindgen_function[ +.get_function[ def fm_add( def fm_mix3( def fm_affine( diff --git a/tests/e2e/fixtures/functional_records/expect.emit.owned_dl_handle.mojo b/tests/e2e/fixtures/functional_records/expect.emit.owned_dl_handle.mojo index 8814009..7979b10 100644 --- a/tests/e2e/fixtures/functional_records/expect.emit.owned_dl_handle.mojo +++ b/tests/e2e/fixtures/functional_records/expect.emit.owned_dl_handle.mojo @@ -1,5 +1,5 @@ def _bindgen_dylib( -_bindgen_function[ +.get_function[ def fr_linear( def fr_apply_mode( def fr_is_nonzero( diff --git a/tests/e2e/fixtures/globals_consts_runtime/expect.emit.external.mojo b/tests/e2e/fixtures/globals_consts_runtime/expect.emit.external.mojo index ab7c558..f6d29bc 100644 --- a/tests/e2e/fixtures/globals_consts_runtime/expect.emit.external.mojo +++ b/tests/e2e/fixtures/globals_consts_runtime/expect.emit.external.mojo @@ -1,6 +1,6 @@ # Each non-comment line must be present in the emitted file. -from std.ffi import external_call, DEFAULT_RTLD, OwnedDLHandle, _DLHandle, _Global, _get_global -from std.builtin.simd import SIMD +from std.ffi import external_call, OwnedDLHandle, _Global +from std.simd import SIMD from std.atomic import Atomic struct GlobalVar[T: Copyable & Deinitable, //, link: StaticString]: struct GlobalConst[T: Copyable & Deinitable, //, link: StaticString]: @@ -10,4 +10,4 @@ comptime gcr_mut = GlobalVar[T=int32_t, link="gcr_mut"] comptime gcr_limit = GlobalConst[T=int32_t, link="gcr_limit"] comptime gcr_vec_const = GlobalConst[T=gcr_vec4, link="gcr_vec_const"] comptime gcr_vec_mut = GlobalVar[T=gcr_vec4, link="gcr_vec_mut"] -# global variable gcr_atomic: Atomic[DType.int32] (atomic global requires manual binding (use Atomic APIs on a pointer)) +# global variable gcr_atomic: Atomic[c_int] (atomic global requires manual binding (use Atomic APIs on a pointer)) diff --git a/tests/e2e/fixtures/globals_consts_runtime/expect.emit.owned_dl_handle.mojo b/tests/e2e/fixtures/globals_consts_runtime/expect.emit.owned_dl_handle.mojo index 622f25a..d0c3a50 100644 --- a/tests/e2e/fixtures/globals_consts_runtime/expect.emit.owned_dl_handle.mojo +++ b/tests/e2e/fixtures/globals_consts_runtime/expect.emit.owned_dl_handle.mojo @@ -1,16 +1,16 @@ # Each non-comment line must be present in the emitted file. -from std.builtin.simd import SIMD +from std.simd import SIMD from std.atomic import Atomic comptime _BINDGEN_LIB_PATH_CANDIDATE: String comptime _BINDGEN_LIB_PATH_ENV = "MOJO_BINDGEN_GLOBALS_CONSTS_RUNTIME_LIBRARY_PATH" -def _bindgen_append_dylib_candidate(mut paths: List[Path], path: String) -> None: +def _bindgen_append_dylib_candidate(mut paths: List[Path], path: String): _bindgen_append_dylib_candidate(paths, _bindgen_env_path(_BINDGEN_LIB_PATH_ENV)) -return _find_dylib[_BINDGEN_LIB_NAME](paths) -def _bindgen_dylib() -> _DLHandle: +return OwnedDLHandle(path) +def _bindgen_dylib() -> Pointer[OwnedDLHandle, MutUntrackedOrigin]: comptime int32_t = Int32 comptime gcr_vec4 = SIMD[DType.float32, 4] comptime gcr_mut = GlobalVar[T=int32_t, link="gcr_mut"] comptime gcr_limit = GlobalConst[T=int32_t, link="gcr_limit"] comptime gcr_vec_const = GlobalConst[T=gcr_vec4, link="gcr_vec_const"] comptime gcr_vec_mut = GlobalVar[T=gcr_vec4, link="gcr_vec_mut"] -# global variable gcr_atomic: Atomic[DType.int32] (atomic global requires manual binding (use Atomic APIs on a pointer)) +# global variable gcr_atomic: Atomic[c_int] (atomic global requires manual binding (use Atomic APIs on a pointer)) diff --git a/tests/e2e/test_e2e_runtime.py b/tests/e2e/test_e2e_runtime.py index 0b787ab..ccfeb94 100644 --- a/tests/e2e/test_e2e_runtime.py +++ b/tests/e2e/test_e2e_runtime.py @@ -237,6 +237,7 @@ def test_golden_runtime_case(case_dir: Path, tmp_path: Path) -> None: [ *mojo, "build", + "--Werror", str(runner_external), "-I", str(tmp_path), @@ -260,6 +261,7 @@ def test_golden_runtime_case(case_dir: Path, tmp_path: Path) -> None: [ *mojo, "build", + "--Werror", str(runner_dl), "-I", str(tmp_path), diff --git a/tests/e2e/test_mojo_12.py b/tests/e2e/test_mojo_12.py new file mode 100644 index 0000000..a1e8d59 --- /dev/null +++ b/tests/e2e/test_mojo_12.py @@ -0,0 +1,248 @@ +"""Compile and execute the Mojo 1.2 FFI upgrade against a real C library.""" + +from __future__ import annotations + +import os +import shutil +import subprocess +from pathlib import Path + +import pytest + +from mojo_bindgen import BindgenOptions, bindgen + +pytestmark = pytest.mark.skipif( + not shutil.which("mojo") or not shutil.which("cc"), reason="requires Mojo and a C compiler" +) + +HEADER = """ +int mf_sum(int count, ...); +void mf_record(int count, ...); +double mf_values(int count, ...); +double mf_mix(int tag, ...); +int mf_value(void); +int mf_missing(void); +extern int mf_counter; +extern const int mf_limit; +typedef struct __attribute__((aligned(32))) MFAligned { double x; } MFAligned; +double mf_aligned_sum(const MFAligned *values); +int mf_aligned_ok(const MFAligned *values); +""" + +SOURCE = """ +#include "modern.h" +#include +#include +#ifndef MF_VALUE +#define MF_VALUE 7 +#endif +int mf_counter = 3; +const int mf_limit = 13; +int mf_value(void) { return MF_VALUE; } +int mf_sum(int count, ...) { + va_list args; va_start(args, count); int result = 0; + for (int i=0; i subprocess.CompletedProcess[str]: + return subprocess.run(command, capture_output=True, text=True, env=env, timeout=60) + + +@pytest.fixture(scope="module") +def ffi_root(tmp_path_factory) -> Path: + root = tmp_path_factory.mktemp("mojo12") + header = root / "modern.h" + header.write_text(HEADER) + source = root / "modern.c" + source.write_text(SOURCE) + for name, value in (("modernffi", 7), ("override", 11)): + result = _run( + [ + "cc", + "-shared", + "-fPIC", + str(source), + f"-DMF_VALUE={value}", + "-o", + str(root / f"lib{name}.so"), + ] + ) + assert result.returncode == 0, result.stderr + for mode, module in ( + ("external_call", "modern_external"), + ("owned_dl_handle", "modern_dynamic"), + ): + result = bindgen( + BindgenOptions( + header=header, + library="modernffi", + link_name="modernffi", + linking=mode, + library_path_hint=str(root / "libmodernffi.so"), + ) + ) + (root / f"{module}.mojo").write_text(result.bindings_source) + return root + + +def _build(root: Path, name: str, source: str, *, direct=False): + path = root / f"{name}.mojo" + path.write_text(source) + command = ["mojo", "build", "--Werror", str(path), "-I", str(root), "-o", str(root / name)] + if direct: + command += ["-Xlinker", f"-L{root}", "-Xlinker", "-lmodernffi"] + return _run(command) + + +def _environment(root: Path) -> dict[str, str]: + env = os.environ.copy() + env["LD_LIBRARY_PATH"] = str(root) + ":" + env.get("LD_LIBRARY_PATH", "") + env.pop("MOJO_BINDGEN_MODERNFFI_LIBRARY_PATH", None) + env.pop("MOJO_BINDGEN_LIBRARY_PATH", None) + return env + + +def test_checked_varargs_and_overaligned_array(ffi_root: Path) -> None: + source = """from modern_external import mf_sum, mf_record, mf_counter, mf_values, mf_mix, MFAligned, mf_aligned_sum, mf_aligned_ok +from std.ffi import c_int +from std.testing import assert_equal + +def main() raises: + assert_equal(mf_sum(c_int(0)), c_int(0)) + mf_record(c_int(0)) + assert_equal(mf_counter.load(), c_int(0)) + mf_record(c_int(2), c_int(3), c_int(5)) + assert_equal(mf_counter.load(), c_int(8)) + assert_equal(mf_sum(c_int(3), c_int(2), c_int(3), c_int(4)), c_int(9)) + assert_equal(mf_values(c_int(1), Float64(1.25)), Float64(1.25)) + assert_equal(mf_values(c_int(2), Float64(1.25), Float64(2.5)), Float64(3.75)) + var value = c_int(6) + assert_equal(mf_mix(c_int(1), c_int(2), Float64(0.5), Pointer(to=value)), Float64(9.5)) + var ptr = Optional(Pointer(to=value)) + assert_equal(mf_mix(c_int(1), c_int(2), Float64(0.5), ptr), Float64(9.5)) + var null_ptr = Optional[Pointer[c_int, MutUntrackedOrigin]](None) + assert_equal(mf_mix(c_int(1), c_int(2), Float64(0.5), null_ptr), Float64(3.5)) + var values = Array[MFAligned, 3](uninitialized=True) + values[0].x = 1.0 + values[1].x = 2.0 + values[2].x = 3.0 + var raw = values.unsafe_ptr().as_imm().unsafe_origin_cast[ImmUntrackedOrigin]() + assert_equal(mf_aligned_ok(raw), c_int(1)) + assert_equal(mf_aligned_sum(raw), Float64(6)) + deinit(values^) +""" + result = _build(ffi_root, "checked", source, direct=True) + assert result.returncode == 0, result.stderr + result = _run([str(ffi_root / "checked")], env=_environment(ffi_root)) + assert result.returncode == 0, result.stderr + + +@pytest.mark.parametrize( + "call, diagnostic", + [ + ("mf_sum()", "missing fixed C arguments"), + ("mf_sum(Int64(1), c_int(1))", "fixed argument 0"), + ("mf_sum(c_int(1), Int8(2))", "promote"), + ("mf_sum(c_int(1), True)", "promote"), + ("mf_values(c_int(1), Float32(1.25))", "promote"), + ('mf_sum(c_int(1), String("unsafe"))', "as_c_string_span"), + ("mf_sum(c_int(1), Optional[c_int](1))", "promote"), + ], +) +def test_varargs_reject_unsafe_arguments(ffi_root: Path, call: str, diagnostic: str) -> None: + result = _build( + ffi_root, + "invalid", + f"from modern_external import mf_sum, mf_values\nfrom std.ffi import c_int\n\ndef main():\n _ = {call}\n", + direct=True, + ) + assert result.returncode != 0 + assert diagnostic in result.stderr, result.stderr + + +def test_dynamic_globals_and_override_precedence(ffi_root: Path) -> None: + source = """from modern_dynamic import mf_value, mf_counter, mf_limit +from std.ffi import c_int +from std.testing import assert_equal + +def main() raises: + assert_equal(mf_counter.load(), c_int(3)) + mf_counter.store(c_int(9)) + assert_equal(mf_counter.ptr()[], c_int(9)) + assert_equal(mf_limit.load(), c_int(13)) + print(mf_value()) +""" + result = _build(ffi_root, "dynamic", source) + assert result.returncode == 0, result.stderr + env = _environment(ffi_root) + result = _run([str(ffi_root / "dynamic")], env=env) + assert result.returncode == 0, result.stderr + assert result.stdout.strip() == "7" + env["MOJO_BINDGEN_LIBRARY_PATH"] = str(ffi_root / "liboverride.so") + result = _run([str(ffi_root / "dynamic")], env=env) + assert result.returncode == 0, result.stderr + assert result.stdout.strip() == "11" + env["MOJO_BINDGEN_MODERNFFI_LIBRARY_PATH"] = str(ffi_root / "libmodernffi.so") + result = _run([str(ffi_root / "dynamic")], env=env) + assert result.returncode == 0, result.stderr + assert result.stdout.strip() == "7" + + +def test_missing_dynamic_symbol_keeps_non_raising_diagnostic(ffi_root: Path) -> None: + result = _build( + ffi_root, + "missing", + "from modern_dynamic import mf_missing\n\ndef main():\n print(mf_missing())\n", + ) + assert result.returncode == 0, result.stderr + result = _run([str(ffi_root / "missing")], env=_environment(ffi_root)) + assert result.returncode != 0 + assert "mf_missing" in result.stdout + result.stderr + assert "modernffi" in result.stdout + result.stderr + + +def test_missing_library_has_candidate_diagnostic(ffi_root: Path) -> None: + result = bindgen( + BindgenOptions( + header=ffi_root / "modern.h", + library="missing_bindgen_test_library", + link_name="missing_bindgen_test_library", + linking="owned_dl_handle", + library_path_hint=str(ffi_root / "does-not-exist.so"), + ) + ) + (ffi_root / "missing_library.mojo").write_text(result.bindings_source) + result = _build( + ffi_root, + "missing_lib", + "from missing_library import mf_value\n\ndef main():\n print(mf_value())\n", + ) + assert result.returncode == 0, result.stderr + result = _run([str(ffi_root / "missing_lib")], env=_environment(ffi_root)) + assert result.returncode != 0 + assert "does-not-exist.so" in result.stdout + result.stderr + assert "missing_bindgen_test_library" in result.stdout + result.stderr diff --git a/tests/surface/fixtures/global_stub_const_ptr/expect.external.mojo b/tests/surface/fixtures/global_stub_const_ptr/expect.external.mojo index 746eb66..a33370a 100644 --- a/tests/surface/fixtures/global_stub_const_ptr/expect.external.mojo +++ b/tests/surface/fixtures/global_stub_const_ptr/expect.external.mojo @@ -1,10 +1,10 @@ # Generated by mojo_bindgen - do not edit by hand. +# Requires Mojo 1.2 or later (1.x). # source: tests/surface/fixtures/global_stub_const_ptr/input.h # library: surface_globals link_name: surface_globals # FFI mode: external_call -from std.ffi import external_call, DEFAULT_RTLD, OwnedDLHandle, _DLHandle, _Global, _get_global -from std.memory.unsafe_pointer import unsafe_cast +from std.ffi import external_call, OwnedDLHandle, _Global from std.os import abort comptime _BINDGEN_LIB_NAME = "surface_globals" @@ -12,71 +12,52 @@ comptime _BINDGEN_LINK_NAME = "surface_globals" def _bindgen_init_dylib() -> OwnedDLHandle: try: - return OwnedDLHandle(DEFAULT_RTLD) + return OwnedDLHandle() except e: abort(t"bindgen: failed to open process dynamic symbol table: {e}") +# Private storage adapter: keep the owner alive for existing free-function bindings. comptime _BINDGEN_DYLIB = _Global["mojo_bindgen/surface_globals", _bindgen_init_dylib] -# Returns a borrowed process-lifetime dynamic library handle; do not close it. -def _bindgen_dylib() -> _DLHandle: - var dylib_ptr = _get_global[ - _BINDGEN_DYLIB.name, - _BINDGEN_DYLIB._init_wrapper, - _BINDGEN_DYLIB._deinit_wrapper, - ]() - var dylib = unsafe_cast[Type=_BINDGEN_DYLIB.StorageType](dylib_ptr).value()[].borrow() - if not dylib: - abort(t"bindgen: failed to load dynamic library '{_BINDGEN_LIB_NAME}'") - return dylib - -def _bindgen_function[Fn: TrivialRegisterPassable](symbol: StringSpan) -> Fn: - var fn_ptr = _bindgen_dylib().get_symbol[NoneType](symbol) - if not fn_ptr: - abort( - t"bindgen: missing C function symbol '{symbol}' " - t"in dynamic library '{_BINDGEN_LIB_NAME}'" - ) - return Pointer(to=fn_ptr.value()).unsafe_bitcast[Fn]()[] +# The cached owner lives until process teardown; do not close it. +def _bindgen_dylib() -> Pointer[OwnedDLHandle, MutUntrackedOrigin]: + try: + return _BINDGEN_DYLIB.get_or_create_ptr() + except e: + abort(t"bindgen: failed to initialize dynamic library '{_BINDGEN_LIB_NAME}': {e}") struct GlobalVar[T: Copyable & Deinitable, //, link: StaticString]: @staticmethod def _raw() -> Pointer[Self.T, MutUntrackedOrigin]: - var opt: Optional[Pointer[Self.T, MutUntrackedOrigin]] = _bindgen_dylib().get_symbol[Self.T](StringSpan(Self.link)) - if not opt: - abort( - t"bindgen: missing C global symbol '{Self.link}' " - t"in dynamic library '{_BINDGEN_LIB_NAME}'" - ) - return opt.value() + var owner = _bindgen_dylib() + if case .Some(ptr) = owner[].get_symbol[Self.T](StringSpan(Self.link)): + return ptr + abort(t"bindgen: missing C global symbol '{Self.link}' in dynamic library '{_BINDGEN_LIB_NAME}'") @staticmethod def ptr() -> Pointer[Self.T, MutUntrackedOrigin]: - return rebind[Pointer[Self.T, MutUntrackedOrigin]](Self._raw()) + return Self._raw() @staticmethod def load() -> Self.T: return Self._raw()[].copy() @staticmethod - def store(value: Self.T) -> None: - var p = rebind[Pointer[Self.T, MutUntrackedOrigin]](Self._raw()) + def store(value: Self.T): + var p = Self._raw() p[] = value.copy() struct GlobalConst[T: Copyable & Deinitable, //, link: StaticString]: @staticmethod - def _raw() -> Pointer[Self.T, MutUntrackedOrigin]: - var opt: Optional[Pointer[Self.T, MutUntrackedOrigin]] = _bindgen_dylib().get_symbol[Self.T](StringSpan(Self.link)) - if not opt: - abort( - t"bindgen: missing C global symbol '{Self.link}' " - t"in dynamic library '{_BINDGEN_LIB_NAME}'" - ) - return opt.value() + def _raw() -> Pointer[Self.T, ImmUntrackedOrigin]: + var owner = _bindgen_dylib().as_imm() + if case .Some(ptr) = owner[].get_symbol[Self.T](StringSpan(Self.link)): + return ptr + abort(t"bindgen: missing C global symbol '{Self.link}' in dynamic library '{_BINDGEN_LIB_NAME}'") @staticmethod def ptr() -> Pointer[Self.T, ImmUntrackedOrigin]: - return rebind[Pointer[Self.T, ImmUntrackedOrigin]](Self._raw()) + return Self._raw() @staticmethod def load() -> Self.T: diff --git a/tests/surface/fixtures/globals_and_consts/expect.external.mojo b/tests/surface/fixtures/globals_and_consts/expect.external.mojo index 877ad9c..3ae3023 100644 --- a/tests/surface/fixtures/globals_and_consts/expect.external.mojo +++ b/tests/surface/fixtures/globals_and_consts/expect.external.mojo @@ -1,10 +1,10 @@ # Generated by mojo_bindgen - do not edit by hand. +# Requires Mojo 1.2 or later (1.x). # source: tests/surface/fixtures/globals_and_consts/input.h # library: surface_globals link_name: surface_globals # FFI mode: external_call -from std.ffi import external_call, DEFAULT_RTLD, OwnedDLHandle, _DLHandle, _Global, _get_global, c_int, c_uint -from std.memory.unsafe_pointer import unsafe_cast +from std.ffi import external_call, OwnedDLHandle, _Global, c_int, c_uint from std.os import abort comptime _BINDGEN_LIB_NAME = "surface_globals" @@ -12,71 +12,52 @@ comptime _BINDGEN_LINK_NAME = "surface_globals" def _bindgen_init_dylib() -> OwnedDLHandle: try: - return OwnedDLHandle(DEFAULT_RTLD) + return OwnedDLHandle() except e: abort(t"bindgen: failed to open process dynamic symbol table: {e}") +# Private storage adapter: keep the owner alive for existing free-function bindings. comptime _BINDGEN_DYLIB = _Global["mojo_bindgen/surface_globals", _bindgen_init_dylib] -# Returns a borrowed process-lifetime dynamic library handle; do not close it. -def _bindgen_dylib() -> _DLHandle: - var dylib_ptr = _get_global[ - _BINDGEN_DYLIB.name, - _BINDGEN_DYLIB._init_wrapper, - _BINDGEN_DYLIB._deinit_wrapper, - ]() - var dylib = unsafe_cast[Type=_BINDGEN_DYLIB.StorageType](dylib_ptr).value()[].borrow() - if not dylib: - abort(t"bindgen: failed to load dynamic library '{_BINDGEN_LIB_NAME}'") - return dylib - -def _bindgen_function[Fn: TrivialRegisterPassable](symbol: StringSpan) -> Fn: - var fn_ptr = _bindgen_dylib().get_symbol[NoneType](symbol) - if not fn_ptr: - abort( - t"bindgen: missing C function symbol '{symbol}' " - t"in dynamic library '{_BINDGEN_LIB_NAME}'" - ) - return Pointer(to=fn_ptr.value()).unsafe_bitcast[Fn]()[] +# The cached owner lives until process teardown; do not close it. +def _bindgen_dylib() -> Pointer[OwnedDLHandle, MutUntrackedOrigin]: + try: + return _BINDGEN_DYLIB.get_or_create_ptr() + except e: + abort(t"bindgen: failed to initialize dynamic library '{_BINDGEN_LIB_NAME}': {e}") struct GlobalVar[T: Copyable & Deinitable, //, link: StaticString]: @staticmethod def _raw() -> Pointer[Self.T, MutUntrackedOrigin]: - var opt: Optional[Pointer[Self.T, MutUntrackedOrigin]] = _bindgen_dylib().get_symbol[Self.T](StringSpan(Self.link)) - if not opt: - abort( - t"bindgen: missing C global symbol '{Self.link}' " - t"in dynamic library '{_BINDGEN_LIB_NAME}'" - ) - return opt.value() + var owner = _bindgen_dylib() + if case .Some(ptr) = owner[].get_symbol[Self.T](StringSpan(Self.link)): + return ptr + abort(t"bindgen: missing C global symbol '{Self.link}' in dynamic library '{_BINDGEN_LIB_NAME}'") @staticmethod def ptr() -> Pointer[Self.T, MutUntrackedOrigin]: - return rebind[Pointer[Self.T, MutUntrackedOrigin]](Self._raw()) + return Self._raw() @staticmethod def load() -> Self.T: return Self._raw()[].copy() @staticmethod - def store(value: Self.T) -> None: - var p = rebind[Pointer[Self.T, MutUntrackedOrigin]](Self._raw()) + def store(value: Self.T): + var p = Self._raw() p[] = value.copy() struct GlobalConst[T: Copyable & Deinitable, //, link: StaticString]: @staticmethod - def _raw() -> Pointer[Self.T, MutUntrackedOrigin]: - var opt: Optional[Pointer[Self.T, MutUntrackedOrigin]] = _bindgen_dylib().get_symbol[Self.T](StringSpan(Self.link)) - if not opt: - abort( - t"bindgen: missing C global symbol '{Self.link}' " - t"in dynamic library '{_BINDGEN_LIB_NAME}'" - ) - return opt.value() + def _raw() -> Pointer[Self.T, ImmUntrackedOrigin]: + var owner = _bindgen_dylib().as_imm() + if case .Some(ptr) = owner[].get_symbol[Self.T](StringSpan(Self.link)): + return ptr + abort(t"bindgen: missing C global symbol '{Self.link}' in dynamic library '{_BINDGEN_LIB_NAME}'") @staticmethod def ptr() -> Pointer[Self.T, ImmUntrackedOrigin]: - return rebind[Pointer[Self.T, ImmUntrackedOrigin]](Self._raw()) + return Self._raw() @staticmethod def load() -> Self.T: diff --git a/tests/surface/fixtures/nested_flexible_tail_terminal/expect.external.mojo b/tests/surface/fixtures/nested_flexible_tail_terminal/expect.external.mojo index dd6d460..f11f735 100644 --- a/tests/surface/fixtures/nested_flexible_tail_terminal/expect.external.mojo +++ b/tests/surface/fixtures/nested_flexible_tail_terminal/expect.external.mojo @@ -6,7 +6,7 @@ comptime uint32_t = UInt32 @align(4) -struct nft_packet_tail: +struct nft_packet_tail(not Movable): var len: uint32_t var payload: Array[c_uchar, 0] @staticmethod @@ -22,7 +22,7 @@ struct nft_packet_tail: return raw.unsafe_offset(4) @align(4) -struct nft_packet_wrapper: +struct nft_packet_wrapper(not Movable): var tag: uint32_t var tail: nft_packet_tail @staticmethod diff --git a/tests/surface/fixtures/non_register_return_stub/expect.external.mojo b/tests/surface/fixtures/non_register_return_stub/expect.external.mojo index c0fefd5..6dfd812 100644 --- a/tests/surface/fixtures/non_register_return_stub/expect.external.mojo +++ b/tests/surface/fixtures/non_register_return_stub/expect.external.mojo @@ -12,5 +12,5 @@ comptime int32_t = Int32 struct nrr_payload(Copyable, Movable): var values: Array[int32_t, 2] -def nrr_build() abi("C") -> nrr_payload: - return external_call["nrr_build", nrr_payload]() +# C return type is not RegisterPassable - Mojo FFI cannot model this return; bind manually. +# nrr_payload nrr_build() diff --git a/tests/surface/fixtures/variadic_stub/expect.external.mojo b/tests/surface/fixtures/variadic_stub/expect.external.mojo index 79cfb70..53028ae 100644 --- a/tests/surface/fixtures/variadic_stub/expect.external.mojo +++ b/tests/surface/fixtures/variadic_stub/expect.external.mojo @@ -7,5 +7,12 @@ from std.ffi import external_call comptime int32_t = Int32 -# variadic C function - not callable from thin FFI: -# int32_t vs_sum(count: int32_t, ...) +def vs_sum[*_BindgenArgs: AnyType](*args: *_BindgenArgs) -> int32_t: + comptime assert _BindgenArgs.length >= 1, "vs_sum: missing fixed C arguments" + comptime if _BindgenArgs.length >= 1: + comptime assert _BindgenArgs[0] == int32_t, "vs_sum: fixed argument 0 must have its declared C type" + comptime for i in range(1, _BindgenArgs.length): + _bindgen_check_vararg[_BindgenArgs[i]]() + return external_call["vs_sum", int32_t, num_fixed_args=1](*args) + else: + abort("bindgen: invalid variadic arity") diff --git a/tests/unit/test_analysis_record_policies.py b/tests/unit/test_analysis_record_policies.py index c0f3a6d..52b778b 100644 --- a/tests/unit/test_analysis_record_policies.py +++ b/tests/unit/test_analysis_record_policies.py @@ -172,7 +172,7 @@ def test_assign_record_policies_marks_fam_struct_memory_only() -> None: assert packet.flexible_tail is not None assert packet.fieldwise_init is False assert packet.passability == MojoPassability.MEMORY_ONLY - assert packet.traits == [] + assert packet.traits == [StructTraits.NOT_MOVABLE] def test_assign_record_policies_keeps_incomplete_struct_copyable_only() -> None: @@ -217,7 +217,7 @@ def test_assign_record_policies_drops_traits_for_representable_atomic_storage() assert atomic_holder.fieldwise_init is False assert atomic_holder.passability == MojoPassability.MEMORY_ONLY - assert atomic_holder.traits == [] + assert atomic_holder.traits == [StructTraits.NOT_MOVABLE] def test_assign_record_policies_keeps_pure_bitfield_struct_non_fieldwise_init() -> None: diff --git a/tests/unit/test_analysis_struct_mapping.py b/tests/unit/test_analysis_struct_mapping.py index 5963e28..b69eee4 100644 --- a/tests/unit/test_analysis_struct_mapping.py +++ b/tests/unit/test_analysis_struct_mapping.py @@ -12,7 +12,6 @@ BitfieldGroupMember, BuiltinType, ByteOrder, - DTypeArg, Field, Initializer, InitializerParam, @@ -32,6 +31,7 @@ StructKind, StructRef, TargetABI, + TypeArg, UnsupportedType, ) @@ -81,7 +81,6 @@ def _context_for(*decls: Struct) -> StructMappingContext: record_map = {decl.decl_id: decl for decl in decls} return StructMappingContext( record_map=record_map, - target_abi=_abi(), type_mapper=MapTypePass(), ) @@ -115,7 +114,6 @@ def fail_if_recomputed(*args, **kwargs): plain_fields=(PlainFieldFact(index=0, byte_offset=0, size_bytes=4, align_bytes=4),), ) }, - target_abi=_abi(), type_mapper=MapTypePass(), ) @@ -472,7 +470,9 @@ def test_map_struct_keeps_atomic_field_types_and_drops_traits() -> None: assert mapped.members[0] == StoredMember( index=0, name="counter", - type=ParametricType(base=ParametricBase.ATOMIC, args=[DTypeArg("DType.int32")]), + type=ParametricType( + base=ParametricBase.ATOMIC, args=[TypeArg(BuiltinType(MojoBuiltin.C_INT))] + ), byte_offset=0, ) diff --git a/tests/unit/test_analysis_type_mapping.py b/tests/unit/test_analysis_type_mapping.py index 3544e97..39634c0 100644 --- a/tests/unit/test_analysis_type_mapping.py +++ b/tests/unit/test_analysis_type_mapping.py @@ -25,6 +25,7 @@ QualifiedType, Qualifiers, StructRef, + TypeArg, TypeRef, UnsupportedType, VectorType, @@ -147,7 +148,7 @@ def test_map_type_maps_representable_atomic_to_atomic_surface_type() -> None: assert map_type(atomic) == ParametricType( base=ParametricBase.ATOMIC, - args=[DTypeArg("DType.uint32")], + args=[TypeArg(BuiltinType(MojoBuiltin.C_UINT))], ) diff --git a/tests/unit/test_analysis_unit_mapping.py b/tests/unit/test_analysis_unit_mapping.py index 0b570d3..481223b 100644 --- a/tests/unit/test_analysis_unit_mapping.py +++ b/tests/unit/test_analysis_unit_mapping.py @@ -282,7 +282,8 @@ def test_map_unit_maps_function_and_global() -> None: global_decl = mapped.decls[1] assert isinstance(fn_decl, FunctionDecl) - assert fn_decl.kind == FunctionKind.VARIADIC_STUB + assert fn_decl.kind == FunctionKind.WRAPPER + assert fn_decl.num_fixed_args == len(fn_decl.params) assert fn_decl.params[0].name == "a0" assert fn_decl.call_target.symbol == "install" assert isinstance(global_decl, GlobalDecl) diff --git a/tests/unit/test_ir_codegen_edges.py b/tests/unit/test_ir_codegen_edges.py index 6ae5f78..b3d7f45 100644 --- a/tests/unit/test_ir_codegen_edges.py +++ b/tests/unit/test_ir_codegen_edges.py @@ -249,17 +249,17 @@ def test_generator_emits_pure_bitfield_struct_as_storage_plus_accessors() -> Non assert "var __bf1: c_uchar" in out assert "var __bf2:" not in out assert "var ready: c_uint" not in out - assert "def __init__(out self):" in out - assert "def __init__(out self, ready: c_uint, state: c_uint, enabled: Bool):" in out + assert "def __init__(out self: Self):" in out + assert "def __init__(out self: Self, ready: c_uint, state: c_uint, enabled: Bool):" in out assert "self.__bf0 = c_uint(0)" in out assert "self.__bf1 = c_uchar(0)" in out assert "self.set_ready(ready)" in out assert "self.set_state(state)" in out assert "self.set_enabled(enabled)" in out - assert "def ready(self) -> c_uint:" in out - assert "def set_ready(mut self, value: c_uint):" in out - assert "def enabled(self) -> Bool:" in out - assert "def set_enabled(mut self, value: Bool):" in out + assert "def ready(self: Self) -> c_uint:" in out + assert "def set_ready(mut self: Self, value: c_uint):" in out + assert "def enabled(self: Self) -> Bool:" in out + assert "def set_enabled(mut self: Self, value: Bool):" in out def test_generator_emits_mixed_struct_bitfields_as_storage_plus_accessors() -> None: @@ -298,8 +298,8 @@ def test_generator_emits_mixed_struct_bitfields_as_storage_plus_accessors() -> N assert "var ready: c_uint" not in out assert "var __bf0: c_uint" in out assert "def __init__(out self" not in out - assert "def ready(self) -> c_uint:" in out - assert "def set_ready(mut self, value: c_uint):" in out + assert "def ready(self: Self) -> c_uint:" in out + assert "def set_ready(mut self: Self, value: c_uint):" in out def test_generator_preserves_typedef_names_for_bitfield_accessors() -> None: @@ -360,10 +360,10 @@ def test_generator_preserves_typedef_names_for_bitfield_accessors() -> None: assert "comptime uint32_t = UInt32" in out assert "var __bf0: c_uint" in out - assert "def ready(self) -> uint32_t:" in out - assert "def set_ready(mut self, value: uint32_t):" in out - assert "def state(self) -> uint32_t:" in out - assert "def set_state(mut self, value: uint32_t):" in out + assert "def ready(self: Self) -> uint32_t:" in out + assert "def set_ready(mut self: Self, value: uint32_t):" in out + assert "def state(self: Self) -> uint32_t:" in out + assert "def set_state(mut self: Self, value: uint32_t):" in out def test_generator_resets_bitfield_storage_after_zero_width_boundary_in_mixed_struct() -> None: @@ -418,8 +418,8 @@ def test_generator_resets_bitfield_storage_after_zero_width_boundary_in_mixed_st ) assert "var __bf0: c_uint" in out assert "var __bf1: c_uint" in out - assert "def left(self) -> c_uint:" in out - assert "def right(self) -> c_uint:" in out + assert "def left(self: Self) -> c_uint:" in out + assert "def right(self: Self) -> c_uint:" in out def test_generator_emits_zero_init_only_for_anonymous_only_pure_bitfield_struct() -> None: @@ -454,8 +454,8 @@ def test_generator_emits_zero_init_only_for_anonymous_only_pure_bitfield_struct( ) ) assert "@fieldwise_init\nstruct OnlyAnonBits" not in out - assert "def __init__(out self):" in out - assert "def __init__(out self," not in out + assert "def __init__(out self: Self):" in out + assert "def __init__(out self: Self," not in out def test_generator_omits_self_alias_typedef_for_complete_union_name() -> None: @@ -577,7 +577,7 @@ def test_generator_renders_global_var_stub_and_macro_comments() -> None: assert ( "external_call" in import_line and "OwnedDLHandle" in import_line and "c_int" in import_line ) - assert "def _bindgen_dylib() -> _DLHandle:" in out + assert "def _bindgen_dylib() -> Pointer[OwnedDLHandle, MutUntrackedOrigin]:" in out assert "struct GlobalVar[T: Copyable & Deinitable, //, link: StaticString]:" in out assert "struct GlobalConst[T: Copyable & Deinitable, //, link: StaticString]:" in out assert 'comptime global_counter = GlobalVar[T=c_int, link="global_counter"]' in out @@ -697,13 +697,9 @@ def test_generator_imports_simd_complex_atomic_and_emits_fallback_notes() -> Non ) unit = Unit(source_header="t.h", library="t", link_name="t", target_abi=_abi(), decls=[st]) out = MojoGenerator(MojoEmitOptions()).generate(unit) - assert "from std.builtin.simd import SIMD" in out - assert "from std.complex import ComplexSIMD" in out - assert "from std.os import Atomic" not in out - assert "var lanes: SIMD[DType.float32, 4]" in out - assert "var complex_value: ComplexSIMD[DType.float32, 1]" in out - assert "var fallback_atomic: Int32" in out - assert "atomic types were mapped to their underlying non-atomic Mojo type" in out + assert "var storage: Array[UInt8, 32]" in out + assert "atomic field `fallback_atomic` has no layout-compatible Atomic[T]" in out + assert "var fallback_atomic: Int32" not in out def test_generator_imports_atomic_for_representable_atomic_types() -> None: @@ -725,7 +721,7 @@ def test_generator_imports_atomic_for_representable_atomic_types() -> None: out = MojoGenerator(MojoEmitOptions()).generate(unit) assert "from std.atomic import Atomic" in out assert ( - "# global variable counter: Atomic[DType.int32] (atomic global requires manual " + "# global variable counter: Atomic[c_int] (atomic global requires manual " "binding (use Atomic APIs on a pointer))" in out ) @@ -775,9 +771,9 @@ def test_generator_omits_copy_traits_and_fieldwise_init_for_atomic_struct_fields assert "@fieldwise_init\nstruct AtomicHolder" not in out assert "struct AtomicHolder(Copyable" not in out assert "struct AtomicHolder(Movable" not in out - assert "struct AtomicHolder:" in out - assert "var counter: Atomic[DType.int32]" in out - assert "var flags: Atomic[DType.uint32]" in out + assert "struct AtomicHolder(not Movable):" in out + assert "var counter: Atomic[c_int]" in out + assert "var flags: Atomic[c_uint]" in out def test_generator_emits_global_const_wrapper_for_const_qualified_scalar() -> None: @@ -939,17 +935,13 @@ def test_generator_emits_function_pointer_return_wrappers_for_both_link_modes() assert "def pfr_select_add() -> pfr_binary_op_t:" in dl_out assert ("def pfr_select_add_direct() -> pfr_select_add_direct_return_cb:") in dl_out assert ( - 'var _bindgen_c_fn = _bindgen_function[def() thin abi("C") -> pfr_binary_op_t](' - 'StringSpan("pfr_select_add"))' + 'var _bindgen_c_fn = _bindgen_owner[].get_function[pfr_binary_op_t]("pfr_select_add")' ) in dl_out assert ( - 'var _bindgen_c_fn = _bindgen_function[def() thin abi("C") -> ' - 'pfr_select_add_direct_return_cb](StringSpan("pfr_select_add_direct"))' in dl_out - ) - assert ( - "var _bindgen_c_fn = _bindgen_function[def(pfr_binary_op_t, c_int, c_int) " - 'thin abi("C") -> c_int](StringSpan("pfr_call"))' in dl_out + "var _bindgen_c_fn = _bindgen_owner[].get_function[" + 'pfr_select_add_direct_return_cb]("pfr_select_add_direct")' in dl_out ) + assert 'var _bindgen_c_fn = _bindgen_owner[].get_function[c_int]("pfr_call")' in dl_out assert "return _bindgen_c_fn(op, lhs, rhs)" in dl_out diff --git a/tests/unit/test_mojo_12_mapping.py b/tests/unit/test_mojo_12_mapping.py new file mode 100644 index 0000000..49c83b0 --- /dev/null +++ b/tests/unit/test_mojo_12_mapping.py @@ -0,0 +1,99 @@ +"""Mojo 1.2 mapping, eligibility, and additive IR contracts.""" + +from pathlib import Path + +import pytest + +from mojo_bindgen import BindgenOptions, bindgen +from mojo_bindgen.analysis.facts.type_layout import type_layout +from mojo_bindgen.analysis.mojo.type_mapping import MapTypePass +from mojo_bindgen.ir import ( + Array, + AtomicType, + BuiltinType, + FunctionDecl, + FunctionKind, + IntKind, + IntType, + MojoBuiltin, + NamedType, + ParametricBase, + ParametricType, + TypeArg, +) + + +def test_atomic_layout_is_not_assumed_transparent() -> None: + value = IntType(IntKind.INT, 4, 4) + atomic = AtomicType(value, size_bytes=16, align_bytes=16) + assert type_layout(atomic) == (16, 16) + assert MapTypePass().run(atomic) == Array(BuiltinType(MojoBuiltin.UINT8), 16) + compatible = AtomicType(value, size_bytes=4, align_bytes=4) + assert MapTypePass().run(compatible) == ParametricType( + ParametricBase.ATOMIC, [TypeArg(BuiltinType(MojoBuiltin.C_INT))] + ) + assert AtomicType.from_json_dict(atomic.to_json_dict()) == atomic + assert AtomicType.from_json_dict( + {"kind": "AtomicType", "value_type": value.to_json_dict()} + ) == AtomicType(value) + + +@pytest.mark.parametrize( + ("target", "flag", "plain"), + [ + ("x86_64-unknown-linux-gnu", None, BuiltinType(MojoBuiltin.C_CHAR)), + ("aarch64-unknown-linux-gnu", None, BuiltinType(MojoBuiltin.C_CHAR)), + ("arm64-apple-macosx", None, BuiltinType(MojoBuiltin.C_CHAR)), + ("x86_64-unknown-linux-gnu", "-funsigned-char", NamedType("UInt8")), + ("aarch64-unknown-linux-gnu", "-fsigned-char", NamedType("Int8")), + ], +) +def test_plain_and_explicit_char_signedness(tmp_path: Path, target, flag, plain) -> None: + header = tmp_path / "chars.h" + header.write_text("void chars(char a, signed char b, unsigned char c);\n") + args = [f"--target={target}"] + ([flag] if flag else []) + result = bindgen(BindgenOptions(header=header, compile_args=args)) + function = next(d for d in result.mojo_module.decls if isinstance(d, FunctionDecl)) + assert [p.type for p in function.params] == [ + plain, + BuiltinType(MojoBuiltin.INT8), + BuiltinType(MojoBuiltin.C_UCHAR), + ] + assert result.unit.target_abi.target_triple + + +@pytest.mark.parametrize("count", [None, 0, 2]) +def test_variadic_metadata_roundtrips_without_breaking_old_ir(count) -> None: + function = FunctionDecl("example", "example", num_fixed_args=count) + payload = function.to_json_dict() + assert FunctionDecl.from_json_dict(payload) == function + if count is None: + assert "num_fixed_args" not in payload + assert FunctionDecl.from_json_dict(payload).num_fixed_args is None + else: + assert payload["num_fixed_args"] == count + + +def test_non_register_return_and_inline_variadic_are_stubs(tmp_path: Path) -> None: + header = tmp_path / "calls.h" + header.write_text( + "struct Payload { int data[4]; };\nstruct Payload make(void);\ninline int local(int n, ...);\n" + ) + result = bindgen(BindgenOptions(header=header)) + functions = {d.name: d for d in result.mojo_module.decls if isinstance(d, FunctionDecl)} + assert functions["make"].kind == FunctionKind.NON_REGISTER_RETURN_STUB + assert functions["local"].kind == FunctionKind.DIRECTIVE_STUB + assert "def make(" not in result.bindings_source + assert "def local(" not in result.bindings_source + + +def test_variadic_link_mode_policy_and_current_keywords(tmp_path: Path) -> None: + header = tmp_path / "calls.h" + header.write_text("int match(int n, ...);\n") + direct = bindgen(BindgenOptions(header=header)) + dynamic = bindgen(BindgenOptions(header=header, linking="owned_dl_handle")) + assert "def match_[*_BindgenArgs: AnyType]" in direct.bindings_source + assert "num_fixed_args=1" in direct.bindings_source + function = next(d for d in dynamic.mojo_module.decls if isinstance(d, FunctionDecl)) + assert function.kind == FunctionKind.VARIADIC_STUB + assert "OwnedDLHandle does not expose variadic" in dynamic.bindings_source diff --git a/tests/unit/test_mojo_ir_json_roundtrip.py b/tests/unit/test_mojo_ir_json_roundtrip.py index ff9ded0..de9f85c 100644 --- a/tests/unit/test_mojo_ir_json_roundtrip.py +++ b/tests/unit/test_mojo_ir_json_roundtrip.py @@ -330,7 +330,7 @@ def test_mojo_module_roundtrip() -> None: dependencies=ModuleDependencies( imports=[ ModuleImport(module="std.ffi", names=["c_int"]), - ModuleImport(module="std.builtin.simd", names=["SIMD"]), + ModuleImport(module="std.simd", names=["SIMD"]), ], support_decls=[SupportDecl(SupportDeclKind.GLOBAL_SYMBOL_HELPERS)], ), diff --git a/tests/unit/test_mojo_ir_printer.py b/tests/unit/test_mojo_ir_printer.py index 6ad763c..bb35d41 100644 --- a/tests/unit/test_mojo_ir_printer.py +++ b/tests/unit/test_mojo_ir_printer.py @@ -320,8 +320,8 @@ def test_render_mojo_module_external_surface_with_synthesized_callback_aliases() assert "comptime ERROR = Flags(c_int(2))" in out assert 'comptime Widget_handler_cb = def (arg0: c_int) thin abi("C") -> c_int' in out assert "var handler: Widget_handler_cb" in out - assert "def enabled(self) -> Bool:" in out - assert "def set_enabled(mut self, value: Bool):" in out + assert "def enabled(self: Self) -> Bool:" in out + assert "def set_enabled(mut self: Self, value: Bool):" in out assert "comptime Packet = UnsafeUnion[c_int, Widget]" in out assert "comptime LIMIT = (1 + 2)" in out assert ( @@ -374,14 +374,14 @@ def test_render_bitfield_accessors_branch_on_target_endianness_comptime() -> Non normalized = normalize_mojo_module(module) assert normalized.dependencies.imports == [ ModuleImport(module="std.ffi", names=["external_call", "c_uint"]), - ModuleImport(module="std.sys.info", names=["is_big_endian"]), + ModuleImport(module="std.sys.info", names=["Endian"]), ] rendered = render_mojo_module(normalized, MojoIRPrintOptions(module_comment=False)) - assert "from std.sys.info import is_big_endian" in rendered + assert "from std.sys.info import Endian" in rendered assert "is_little_endian" not in rendered - assert "comptime if is_big_endian():" in rendered + assert "comptime if Endian.native() == Endian.big:" in rendered assert "else:" in rendered assert "self.__bf0 >> 0" in rendered assert "self.__bf0 >> 1" in rendered @@ -537,9 +537,9 @@ def test_normalize_mojo_module_makes_callback_hoisting_and_imports_explicit() -> assert normalized.dependencies.imports == [ ModuleImport( module="std.ffi", - names=["OwnedDLHandle", "_DLHandle", "_Global", "_find_dylib", "_get_global", "c_int"], + names=["OwnedDLHandle", "_Global", "c_int"], ), - ModuleImport(module="std.memory.unsafe_pointer", names=["unsafe_cast"]), + ModuleImport(module="std.sys.info", names=["CompilationTarget", "platform_map"]), ModuleImport(module="std.os", names=["abort", "getenv"]), ModuleImport(module="std.pathlib", names=["Path"]), ] @@ -588,16 +588,12 @@ def test_normalize_mojo_module_coalesces_seeded_and_discovered_dependencies() -> module="std.ffi", names=[ "external_call", - "DEFAULT_RTLD", "OwnedDLHandle", - "_DLHandle", "_Global", - "_get_global", "c_int", ], ), ModuleImport(module="std.sys.info", names=["size_of"]), - ModuleImport(module="std.memory.unsafe_pointer", names=["unsafe_cast"]), ModuleImport(module="std.os", names=["abort"]), ] assert normalized.dependencies.support_decls == [ @@ -633,11 +629,8 @@ def test_render_mojo_module_uses_owned_dl_handle_library_path_hint() -> None: assert 'comptime _BINDGEN_LIB_PATH_ENV = "MOJO_BINDGEN_DEMO_LIBRARY_PATH"' in rendered assert 'comptime _BINDGEN_GENERIC_LIB_PATH_ENV = "MOJO_BINDGEN_LIBRARY_PATH"' in rendered assert "def _bindgen_env_path(name: String) -> String:" in rendered - assert "def _bindgen_pixi_env_lib_path(subdir: String) -> String:" in rendered - assert ( - "def _bindgen_append_dylib_candidate(mut paths: List[Path], path: String) -> None:" - in rendered - ) + assert 'var project_root = _bindgen_env_path("PIXI_PROJECT_ROOT")' in rendered + assert "def _bindgen_append_dylib_candidate(mut paths: List[Path], path: String):" in rendered assert 'if path != "":' in rendered assert "paths.append(Path(path))" in rendered assert "def _bindgen_dylib_candidates() -> List[Path]:" in rendered @@ -650,17 +643,12 @@ def test_render_mojo_module_uses_owned_dl_handle_library_path_hint() -> None: "_bindgen_env_path(_BINDGEN_GENERIC_LIB_PATH_ENV))" in rendered ) assert "_BINDGEN_LIB_PATH_CANDIDATE" in rendered - assert ( - '_bindgen_prefix_lib_path("CONDA_PREFIX", "lib/lib" + String(_BINDGEN_LINK_NAME) + ".so")' - in rendered - ) - assert '_bindgen_pixi_env_lib_path("lib/lib" + String(_BINDGEN_LINK_NAME) + ".so")' in rendered - assert '"lib" + String(_BINDGEN_LINK_NAME) + ".dylib"' in rendered - assert '"/opt/homebrew/lib/lib" + String(_BINDGEN_LINK_NAME) + ".dylib"' in rendered - assert '"/usr/lib/x86_64-linux-gnu/lib" + String(_BINDGEN_LINK_NAME) + ".so.1"' in rendered - assert "var paths = _bindgen_dylib_candidates()" in rendered - assert "return _find_dylib[_BINDGEN_LIB_NAME](paths)" in rendered - assert "def _bindgen_dylib() -> _DLHandle:" in rendered + assert 'var prefix = _bindgen_env_path("CONDA_PREFIX")' in rendered + assert 'platform_map["shared library suffix", linux=".so", macos=".dylib"]()' in rendered + assert 'paths.append(Path("/opt/homebrew/lib/" + filename))' in rendered + assert "return OwnedDLHandle(path)" in rendered + assert "_find_dylib" not in rendered + assert "def _bindgen_dylib() -> Pointer[OwnedDLHandle, MutUntrackedOrigin]:" in rendered def test_render_owned_dl_handle_sanitizes_library_path_env_var() -> None: @@ -723,8 +711,7 @@ def test_render_owned_dl_handle_function_local_does_not_collide_with_parameters( in rendered ) assert ( - "var _bindgen_c_fn_1 = _bindgen_function[def(execute_on_thread_cb, c_int) " - 'thin abi("C") -> NoneType](StringSpan("execute_on_thread"))' + 'var _bindgen_c_fn_1 = _bindgen_owner[].get_function[NoneType]("execute_on_thread")' ) in rendered assert "_bindgen_c_fn_1(fn_, _bindgen_c_fn)" in rendered assert "var fn_ =" not in rendered @@ -965,8 +952,8 @@ def test_printer_renders_lowered_struct_layout_members_without_normalize_inferen assert "var __pad0: UInt32" in rendered assert "var storage: Array[UInt8, 5]" in rendered assert "var __bf0: c_uchar" in rendered - assert "def enabled(self) -> Bool:" in rendered - assert "def set_enabled(mut self, value: Bool):" in rendered + assert "def enabled(self: Self) -> Bool:" in rendered + assert "def set_enabled(mut self: Self, value: Bool):" in rendered @pytest.mark.skipif(shutil.which("pixi") is None, reason="requires pixi with mojo toolchain") @@ -1111,7 +1098,7 @@ def test_rendered_mojo_module_compiles_with_mixed_decl_kinds(tmp_path: Path) -> ) assert proc.returncode == 0, proc.stderr - assert "def _bindgen_dylib() -> _DLHandle:" in rendered + assert "def _bindgen_dylib() -> Pointer[OwnedDLHandle, MutUntrackedOrigin]:" in rendered assert "struct GlobalVar[T: Copyable & Deinitable, //, link: StaticString]:" in rendered assert 'def install(cb: binary_cb_t) abi("C") -> None:' in rendered assert "def load_widget() -> Pointer[Widget, MutUntrackedOrigin]:" in rendered diff --git a/tests/unit/test_type_lowering.py b/tests/unit/test_type_lowering.py index 8490be0..9deaff1 100644 --- a/tests/unit/test_type_lowering.py +++ b/tests/unit/test_type_lowering.py @@ -552,7 +552,7 @@ def test_type_lowering_recovers_vector_lane_count_for_vector_size_typedef(tmp_pa assert payload.fields[0].type.canonical.count == 4 out = MojoGenerator(MojoEmitOptions()).generate(unit) - assert "from std.builtin.simd import SIMD" in out + assert "from std.simd import SIMD" in out assert "comptime vet_float4 = SIMD[DType.float32, 4]" in out