diff --git a/CHANGELOG.md b/CHANGELOG.md index 3930edd4..612b8e85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ Detailed changelog for Perry. See CLAUDE.md for concise summaries. +## v0.5.909 — feat: add `libDirs` to native library manifest (#762). New optional `targets..libDirs` field in `package.json`'s `perry.nativeLibrary` block — array of linker search paths emitted ahead of the `libs` list. **Surface.** Documented in `docs/src/native-libraries/manifest-v1.md` alongside the existing `libs` / `frameworks` / `pkgConfig` entries. Use it when a wrapper ships a vendored `.a`/`.dylib` outside the cargo crate (the standard `lib_name` lookup probes `target//release/`); previously the only escape hatch was an absolute path glued into `libs` via a workaround. **Plumbing.** Three edits — (1) `TargetNativeConfig.lib_dirs: Vec` field added in `crates/perry/src/commands/compile.rs`; (2) `parse_native_library_manifest` parses `libDirs` and anchors relative entries to `package_dir` via `package_dir.join(p)` — mirrors the existing `swift_sources` / `metal_sources` pattern so a `"./vendor/lib"` in the manifest resolves against the wrapper's directory, not the user's cwd (absolute entries pass through unchanged since `PathBuf::join` ignores the base on an absolute right-hand side); (3) `build_and_run_link` in `crates/perry/src/commands/compile/link.rs` emits each entry between the `frameworks` loop and the `libs` loop — `-L` on every non-Windows target, `/LIBPATH:` when `is_windows` (mirrors the `libs` loop's existing `{lib}.lib` vs `-l{lib}` MSVC/Unix split, so a `targets.windows.libDirs` entry actually resolves the `{lib}.lib` lookups instead of being a silent no-op against link.exe). **Validation.** Two new unit tests in `crates/perry/src/commands/compile/resolve.rs::manifest_parse_tests` — `lib_dirs_relative_paths_anchored_to_package_dir` round-trips a manifest with both `"vendor/lib"` and `"/abs/path"` and asserts the package_dir-anchored / pass-through behavior; `lib_dirs_defaults_to_empty_when_absent` confirms the field is optional. `cargo build --release` clean; `cargo test -p perry --lib` green. **Version-bump note.** Renumbered v0.5.908 → v0.5.909 because #760 (test: argon2 + ethers parity) landed on main mid-review and took the v0.5.908 slot. **Contributor.** Implementation by @Lebei2046; MSVC `/LIBPATH:` branch + `package_dir` anchoring + tests + manifest doc entry + this changelog folded in at merge time. + ## v0.5.908 — test: #698 — behavioral parity tests for argon2 + ethers integrations (#760). Follow-up to #694, addressing #698. Adds two new parity fixtures and a `@covers` block on `test_parity_crypto.ts`, moving 18 FFI entries out of `test_ffi_surface_stdlib_integrations.ts` (156 → 138 unique names) into behavioral coverage. **`test_parity_argon2.ts`** — round-trips `argon2.hash` / `argon2.verify` against the `perry-ext-argon2` wrapper; non-deterministic salt is handled by shape-checking the `$argon2id$` prefix and asserting verify round-trips against a freshly produced hash. Only the async path is in `NativeModSig`, so this covers the two FFI entries (`js_argon2_hash`, `js_argon2_verify`) reachable end-to-end. **`test_parity_ethers.ts`** — pure deterministic helpers from `perry-ext-ethers`: `getAddress` (EIP-55 checksum on lower- and upper-case input), `parseEther`/`formatEther` round-trip on 1.5 ETH, `parseUnits`/`formatUnits` at 6- (USDC) and 9-decimal (gwei) positions. Covers the five `js_ethers_*` helpers. Both new fixtures use the `test-parity/expected/` mechanism — Perry routes the npm-style imports to the bundled `perry-ext-*` wrappers, but Node can't resolve the same names without `node_modules`, so they fall through to stored expected-output comparison. **`test_parity_crypto.ts`** — adds a `@covers` block declaring the crypto/webcrypto/crypto_e2e FFI surface it already exercises via `node:crypto` and `crypto.subtle` (digest, hash, pbkdf2/hkdf, subtle.sign/verify); no behavioral change. **Out of scope (each gets its own follow-up).** `jsonwebtoken.sign` — `NativeModSig` passes `payload`/`secret` as `NA_F64` (NaN-boxed) but `js_jwt_sign` reads them as `*const StringHeader`, producing garbage tokens (calling-convention mismatch, same shape as the #591 argon2 fix). `bcrypt.hash` — return value reports `typeof === "object"` in user code; `compare` round-trips correctly but `.startsWith(...)` on the hash fails. `cheerio` — `$.select(...)` returns a bare-number handle, so `.length()` / `.first()` etc. fail with `(number).method is not a function` at the prototype check. Service-backed integrations (MongoDB, MySQL, PG, ioredis, Nodemailer, Sharp) also remain in the inventory pending the local-container / opt-in test setup described in #698. **Audit.** `./test-coverage/audit.sh --markdown` keeps TS-side and combined FFI coverage at **1791/1791 (100.0%)**. `test_ffi_surface_stdlib_integrations.ts` re-regenerated via `regen_ts_surface_inventory.py` to fold in the new `@covers` blocks. Comment-only changes; no behavioral or runtime impact. Contributor: @TheHypnoo. ## v0.5.907 — test(stdlib): #697 — move stdlib_io FFI coverage into behavioral tests (#758). Follow-up to #694: relocates the 155 inventory entries from `test_ffi_surface_stdlib_io.ts` into `@covers` annotations attached to the existing behavioral fixtures for each surface — `fetch.rs` (43 names) → `test_gap_fetch_response.ts`; `streams.rs` (37) → `test_parity_stream_web.ts`; `http.rs` (13) → `test_parity_http.ts`; `net/mod.rs` (10) → `test_net_socket.ts`; `readline.rs` (5) → `test_parity_readline.ts`; `worker_threads.rs` (6) → `test_parity_worker_threads.ts`; `ws.rs` (11) → `test_node_http_ws_upgrade.ts`; `framework/{multipart,request,response,server}.rs` (28) → `test_fastify_integration.ts`. Each receiving fixture already exercises real behavior on its surface (local loopback servers / deterministic in-memory probes / Headers + Blob + Response API roundtrips) — no external services added. **Mechanism.** `regen_ts_surface_inventory.py` excludes the six `test_ffi_surface_*.ts` inventory files when scanning for `@covers` references, so any FFI name that appears in a non-inventory fixture's `@covers` block is dropped from its inventory file on the next regen. After this PR, `test_ffi_surface_stdlib_io.ts` shrinks from **155 → 0** unique FFI names. **Audit.** `./test-coverage/audit.sh --markdown` keeps TypeScript and combined FFI coverage at **1791/1791 (100.0%)**. `test_ffi_surface_runtime_core.ts` re-regenerated against current `main` post-rebase — the inventory drift visible in the diff (added `js_array_flat_depth`, `js_template_raw`, `js_path_matches_glob`, `js_async_first_call`, the 7 new `webassembly.rs` exports; removed `js_mul`, `js_net_socket_destroy/end/write`, the 9 `js_ws_*` stubs from `stdlib_stubs.rs`) reflects upstream Rust refactors, not hand edits. Comment-only changes; no behavioral or runtime impact. Contributor: @TheHypnoo. diff --git a/CLAUDE.md b/CLAUDE.md index 9d2bcdbe..c2955582 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.908 +**Current Version:** 0.5.909 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 9ed2bdbd..7c63f71b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4790,7 +4790,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.908" +version = "0.5.909" dependencies = [ "anyhow", "base64", @@ -4845,14 +4845,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.908" +version = "0.5.909" dependencies = [ "serde", ] [[package]] name = "perry-codegen" -version = "0.5.908" +version = "0.5.909" dependencies = [ "anyhow", "log", @@ -4865,7 +4865,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.908" +version = "0.5.909" dependencies = [ "anyhow", "perry-hir", @@ -4874,7 +4874,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.908" +version = "0.5.909" dependencies = [ "anyhow", "perry-hir", @@ -4882,7 +4882,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.908" +version = "0.5.909" dependencies = [ "anyhow", "perry-dispatch", @@ -4892,7 +4892,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.908" +version = "0.5.909" dependencies = [ "anyhow", "perry-hir", @@ -4901,7 +4901,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.908" +version = "0.5.909" dependencies = [ "anyhow", "base64", @@ -4914,7 +4914,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.908" +version = "0.5.909" dependencies = [ "anyhow", "perry-hir", @@ -4922,7 +4922,7 @@ dependencies = [ [[package]] name = "perry-diagnostics" -version = "0.5.908" +version = "0.5.909" dependencies = [ "serde", "serde_json", @@ -4930,7 +4930,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.908" +version = "0.5.909" [[package]] name = "perry-doc-fixture-my-bindings" @@ -4941,7 +4941,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.908" +version = "0.5.909" dependencies = [ "anyhow", "clap", @@ -4956,7 +4956,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.908" +version = "0.5.909" dependencies = [ "argon2", "perry-ffi", @@ -4964,7 +4964,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.908" +version = "0.5.909" dependencies = [ "perry-ffi", "reqwest", @@ -4973,7 +4973,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.908" +version = "0.5.909" dependencies = [ "bcrypt", "perry-ffi", @@ -4981,7 +4981,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.908" +version = "0.5.909" dependencies = [ "perry-ffi", "rusqlite", @@ -4989,7 +4989,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.908" +version = "0.5.909" dependencies = [ "perry-ffi", "scraper", @@ -4997,14 +4997,14 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.908" +version = "0.5.909" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-cron" -version = "0.5.908" +version = "0.5.909" dependencies = [ "chrono", "cron", @@ -5013,7 +5013,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.908" +version = "0.5.909" dependencies = [ "chrono", "perry-ffi", @@ -5021,7 +5021,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.908" +version = "0.5.909" dependencies = [ "perry-ffi", "rust_decimal", @@ -5029,7 +5029,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.908" +version = "0.5.909" dependencies = [ "perry-ffi", "serde_json", @@ -5037,7 +5037,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.908" +version = "0.5.909" dependencies = [ "perry-ffi", "rand 0.8.6", @@ -5045,21 +5045,21 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.908" +version = "0.5.909" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.908" +version = "0.5.909" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.908" +version = "0.5.909" dependencies = [ "bytes", "http-body-util", @@ -5073,7 +5073,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.908" +version = "0.5.909" dependencies = [ "lazy_static", "perry-ffi", @@ -5084,7 +5084,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.908" +version = "0.5.909" dependencies = [ "lazy_static", "perry-ext-http-server", @@ -5096,7 +5096,7 @@ dependencies = [ [[package]] name = "perry-ext-http-server" -version = "0.5.908" +version = "0.5.909" dependencies = [ "bytes", "http-body-util", @@ -5115,7 +5115,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.908" +version = "0.5.909" dependencies = [ "lazy_static", "perry-ffi", @@ -5125,7 +5125,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.908" +version = "0.5.909" dependencies = [ "base64", "jsonwebtoken", @@ -5136,7 +5136,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.908" +version = "0.5.909" dependencies = [ "lru", "perry-ffi", @@ -5144,7 +5144,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.908" +version = "0.5.909" dependencies = [ "chrono", "perry-ffi", @@ -5152,7 +5152,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.908" +version = "0.5.909" dependencies = [ "bson", "futures-util", @@ -5164,7 +5164,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.908" +version = "0.5.909" dependencies = [ "chrono", "perry-ffi", @@ -5174,7 +5174,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.908" +version = "0.5.909" dependencies = [ "nanoid", "perry-ffi", @@ -5183,7 +5183,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.908" +version = "0.5.909" dependencies = [ "perry-ffi", "rustls", @@ -5194,7 +5194,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.908" +version = "0.5.909" dependencies = [ "lettre", "perry-ffi", @@ -5204,7 +5204,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.908" +version = "0.5.909" dependencies = [ "perry-ffi", "sqlx", @@ -5213,7 +5213,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.908" +version = "0.5.909" dependencies = [ "governor", "perry-ffi", @@ -5221,7 +5221,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.908" +version = "0.5.909" dependencies = [ "base64", "image", @@ -5230,14 +5230,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.908" +version = "0.5.909" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.908" +version = "0.5.909" dependencies = [ "lazy_static", "perry-ffi", @@ -5245,7 +5245,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.908" +version = "0.5.909" dependencies = [ "perry-ffi", "uuid", @@ -5253,7 +5253,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.908" +version = "0.5.909" dependencies = [ "perry-ffi", "regex", @@ -5263,7 +5263,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.908" +version = "0.5.909" dependencies = [ "futures-util", "lazy_static", @@ -5274,7 +5274,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.908" +version = "0.5.909" dependencies = [ "flate2", "perry-ffi", @@ -5282,7 +5282,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.908" +version = "0.5.909" dependencies = [ "dashmap 6.1.0", "once_cell", @@ -5291,7 +5291,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.908" +version = "0.5.909" dependencies = [ "anyhow", "perry-api-manifest", @@ -5305,7 +5305,7 @@ dependencies = [ [[package]] name = "perry-jsruntime" -version = "0.5.908" +version = "0.5.909" dependencies = [ "anyhow", "deno_core", @@ -5325,7 +5325,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.908" +version = "0.5.909" dependencies = [ "anyhow", "perry-diagnostics", @@ -5337,7 +5337,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.908" +version = "0.5.909" dependencies = [ "anyhow", "base64", @@ -5361,7 +5361,7 @@ dependencies = [ [[package]] name = "perry-stdlib" -version = "0.5.908" +version = "0.5.909" dependencies = [ "aes", "aes-gcm", @@ -5429,7 +5429,7 @@ dependencies = [ [[package]] name = "perry-transform" -version = "0.5.908" +version = "0.5.909" dependencies = [ "anyhow", "perry-hir", @@ -5439,7 +5439,7 @@ dependencies = [ [[package]] name = "perry-types" -version = "0.5.908" +version = "0.5.909" dependencies = [ "anyhow", "thiserror 1.0.69", @@ -5447,11 +5447,11 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.908" +version = "0.5.909" [[package]] name = "perry-ui-android" -version = "0.5.908" +version = "0.5.909" dependencies = [ "itoa", "jni", @@ -5466,7 +5466,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.908" +version = "0.5.909" dependencies = [ "rand 0.8.6", "serde", @@ -5476,7 +5476,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.908" +version = "0.5.909" dependencies = [ "cairo-rs", "dirs 5.0.1", @@ -5486,6 +5486,7 @@ dependencies = [ "libc", "libshumate", "mpris-server", + "perry-ffi", "perry-runtime", "perry-ui", "perry-ui-testkit", @@ -5495,7 +5496,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.908" +version = "0.5.909" dependencies = [ "block2", "libc", @@ -5510,7 +5511,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.908" +version = "0.5.909" dependencies = [ "block2", "libc", @@ -5528,11 +5529,11 @@ version = "0.1.0" [[package]] name = "perry-ui-testkit" -version = "0.5.908" +version = "0.5.909" [[package]] name = "perry-ui-tvos" -version = "0.5.908" +version = "0.5.909" dependencies = [ "block2", "libc", @@ -5547,7 +5548,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.908" +version = "0.5.909" dependencies = [ "block2", "libc", @@ -5562,7 +5563,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.908" +version = "0.5.909" dependencies = [ "block2", "libc", @@ -5575,7 +5576,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.908" +version = "0.5.909" dependencies = [ "libc", "perry-runtime", @@ -5589,7 +5590,7 @@ dependencies = [ [[package]] name = "perry-updater" -version = "0.5.908" +version = "0.5.909" dependencies = [ "base64", "ed25519-dalek", @@ -5603,7 +5604,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.908" +version = "0.5.909" dependencies = [ "wasmi", ] @@ -8600,9 +8601,9 @@ dependencies = [ [[package]] name = "wasmi" -version = "0.51.4" +version = "0.51.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc0120da23bc519b1d573e64ffe2a91390468293242db4edc5517f481d7e495d" +checksum = "bb321403ce594274827657a908e13d1d9918aa02257b8bf8391949d9764023ff" dependencies = [ "spin", "wasmi_collections", diff --git a/Cargo.toml b/Cargo.toml index eddb8a1c..fd17d855 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -190,7 +190,7 @@ opt-level = "s" # Optimize for size in stdlib opt-level = 3 [workspace.package] -version = "0.5.908" +version = "0.5.909" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/crates/perry-api-manifest/src/entries.rs b/crates/perry-api-manifest/src/entries.rs index 4040c402..5a0626fa 100644 --- a/crates/perry-api-manifest/src/entries.rs +++ b/crates/perry-api-manifest/src/entries.rs @@ -1874,6 +1874,8 @@ pub static API_MANIFEST: &[ApiEntry] = &[ method("perry/system", "audioGetPeak", false, None), method("perry/system", "audioGetWaveform", false, None), method("perry/system", "audioSetOutputFilename", false, None), + method("perry/system", "audioRegisterCallback", false, None), + method("perry/system", "audioUnregisterCallback", false, None), method("perry/system", "audioStartRecording", false, None), method("perry/system", "audioStopRecording", false, None), // --- perry/system geolocation + image picker (issue #552). --- diff --git a/crates/perry-dispatch/src/lib.rs b/crates/perry-dispatch/src/lib.rs index b09554f8..077ccc91 100644 --- a/crates/perry-dispatch/src/lib.rs +++ b/crates/perry-dispatch/src/lib.rs @@ -2210,6 +2210,18 @@ pub static PERRY_SYSTEM_TABLE: &[MethodRow] = &[ args: &[ArgKind::Str], ret: ReturnKind::Void, }, + MethodRow { + method: "audioRegisterCallback", + runtime: "perry_system_audio_register_callback", + args: &[ArgKind::Closure], + ret: ReturnKind::Void, + }, + MethodRow { + method: "audioUnregisterCallback", + runtime: "perry_system_audio_unregister_callback", + args: &[], + ret: ReturnKind::Void, + }, MethodRow { method: "audioStartRecording", runtime: "perry_system_audio_start_recording", diff --git a/crates/perry-runtime/src/lib.rs b/crates/perry-runtime/src/lib.rs index 1239afa3..b3329694 100644 --- a/crates/perry-runtime/src/lib.rs +++ b/crates/perry-runtime/src/lib.rs @@ -169,6 +169,35 @@ pub use value::{ js_set_native_module_js_loader, js_set_new_from_handle_v8, }; +// Extension pump registration — allows extensions to register pump functions +// that run on each timer tick without hard-link dependencies. +mod ext_pump { + use std::ptr::null_mut; + use std::sync::atomic::{AtomicPtr, Ordering}; + + static EXT_PUMP_FN: AtomicPtr<()> = AtomicPtr::new(null_mut()); + + /// Register an extension's process_pending function pointer. + /// Called by extensions during initialization. + #[no_mangle] + pub extern "C" fn js_register_ext_pump(f: extern "C" fn() -> i32) { + EXT_PUMP_FN.store(f as *mut (), Ordering::Release); + } + + /// Run the registered extension pump if available. Safe to call even if no + /// extension is linked (no-op in that case). + #[no_mangle] + pub extern "C" fn js_run_ext_pump() { + let f = EXT_PUMP_FN.load(Ordering::Acquire); + if !f.is_null() { + unsafe { + let func: extern "C" fn() -> i32 = std::mem::transmute(f); + func(); + } + } + } +} + // Stdlib pump registration — allows perry-ui-macos pump timer to call // js_stdlib_process_pending without a hard link dependency on perry-stdlib. mod stdlib_pump { diff --git a/crates/perry-ui-gtk4/Cargo.toml b/crates/perry-ui-gtk4/Cargo.toml index 72fe403d..2ab7c51f 100644 --- a/crates/perry-ui-gtk4/Cargo.toml +++ b/crates/perry-ui-gtk4/Cargo.toml @@ -10,6 +10,7 @@ crate-type = ["rlib", "staticlib"] perry-ui = { path = "../perry-ui" } perry-ui-testkit = { workspace = true } perry-runtime = { path = "../perry-runtime" } +perry-ffi = { path = "../perry-ffi" } libc = "0.2" gtk4 = { version = "0.9", features = ["v4_6"] } cairo-rs = "0.20" diff --git a/crates/perry-ui-gtk4/src/app.rs b/crates/perry-ui-gtk4/src/app.rs index 57885f8c..36a9cb54 100644 --- a/crates/perry-ui-gtk4/src/app.rs +++ b/crates/perry-ui-gtk4/src/app.rs @@ -71,6 +71,9 @@ extern "C" { fn js_nanbox_get_pointer(value: f64) -> i64; fn js_run_stdlib_pump(); fn js_promise_run_microtasks() -> i32; + fn js_callback_timer_tick() -> i32; + fn js_interval_timer_tick() -> i32; + fn js_run_ext_pump(); } /// Extract a &str from a *const StringHeader pointer. @@ -287,6 +290,19 @@ pub fn app_run(_app_handle: i64) { } }); + // Start the timer pump to drive setInterval/setTimeout callbacks (~120Hz) + // This mirrors the implementation on other platforms (macOS, Windows, etc.) + glib::timeout_add_local(std::time::Duration::from_millis(8), move || { + unsafe { + js_callback_timer_tick(); + js_interval_timer_tick(); + js_run_ext_pump(); + js_run_stdlib_pump(); + js_promise_run_microtasks(); + } + glib::ControlFlow::Continue + }); + // Call on_activate callback ON_ACTIVATE_CALLBACK.with(|cb| { if let Some(callback) = *cb.borrow() { diff --git a/crates/perry-ui-gtk4/src/audio.rs b/crates/perry-ui-gtk4/src/audio.rs index c28cb8b6..9401b7e3 100644 --- a/crates/perry-ui-gtk4/src/audio.rs +++ b/crates/perry-ui-gtk4/src/audio.rs @@ -21,6 +21,8 @@ static mut WAVEFORM_BUFFER: [f64; WAVEFORM_SIZE] = [0.0; WAVEFORM_SIZE]; static RUNNING: AtomicBool = AtomicBool::new(false); +static AUDIO_CALLBACK: Mutex> = Mutex::new(None); + // Recording state static RECORDING: AtomicBool = AtomicBool::new(false); static RECORDED_SAMPLES: Mutex> = Mutex::new(Vec::new()); @@ -197,6 +199,9 @@ extern "C" { fn js_string_from_bytes(ptr: *const u8, len: i32) -> i64; fn js_array_create() -> i64; fn js_array_push_f64(array_ptr: i64, value: f64); + fn js_closure_call2(closure: *const u8, arg1: f64, arg2: f64); + fn js_nanbox_get_pointer(value: f64) -> i64; + fn js_nanbox_pointer(ptr: i64) -> f64; } pub fn set_output_filename(filename: &str) { @@ -237,10 +242,6 @@ pub fn start() -> i64 { return 1; } - if !OUTPUT_FILENAME.lock().unwrap().is_empty() { - start_recording(); - } - RUNNING.store(true, Ordering::Relaxed); std::thread::spawn(|| { @@ -310,6 +311,8 @@ pub fn start() -> i64 { RECORDED_SAMPLES.lock().unwrap().extend_from_slice(&buf); } + invoke_audio_callback(buf.as_ptr(), buffer_frames); + let mut sum_sq = 0.0f64; let mut peak = 0.0f32; @@ -400,3 +403,27 @@ pub fn get_device_model() -> i64 { }; unsafe { js_string_from_bytes(model.as_ptr(), model.len() as i32) } } + +pub fn register_audio_callback(callback: f64) { + *AUDIO_CALLBACK.lock().unwrap() = Some(callback); +} + +pub fn unregister_audio_callback() { + *AUDIO_CALLBACK.lock().unwrap() = None; +} + +pub fn invoke_audio_callback(samples_ptr: *const f32, num_samples: usize) { + let callback_opt = *AUDIO_CALLBACK.lock().unwrap(); + if callback_opt.is_none() { + return; + } + let callback = callback_opt.unwrap(); + let callback_ptr = unsafe { js_nanbox_get_pointer(callback) } as *const u8; + + let samples_val = unsafe { js_nanbox_pointer(samples_ptr as i64) }; + let num_samples_val = num_samples as f64; + + unsafe { + js_closure_call2(callback_ptr, samples_val, num_samples_val); + } +} diff --git a/crates/perry-ui-gtk4/src/lib.rs b/crates/perry-ui-gtk4/src/lib.rs index c23a0853..fcd66f81 100644 --- a/crates/perry-ui-gtk4/src/lib.rs +++ b/crates/perry-ui-gtk4/src/lib.rs @@ -1951,18 +1951,8 @@ pub extern "C" fn perry_system_get_device_model() -> i64 { } #[no_mangle] pub extern "C" fn perry_system_audio_set_output_filename(filename_ptr: i64) { - fn str_from_header(ptr: *const u8) -> &'static str { - if ptr.is_null() { - return ""; - } - unsafe { - let header = ptr as *const perry_runtime::string::StringHeader; - let len = (*header).byte_len as usize; - let data = ptr.add(std::mem::size_of::()); - std::str::from_utf8_unchecked(std::slice::from_raw_parts(data, len)) - } - } - let filename = str_from_header(filename_ptr as *const u8); + let filename_handle = unsafe { perry_ffi::JsString::from_raw(filename_ptr as *mut _) }; + let filename = perry_ffi::read_string(filename_handle).unwrap_or(""); audio::set_output_filename(filename); } #[no_mangle] @@ -1973,6 +1963,16 @@ pub extern "C" fn perry_system_audio_start_recording() { pub extern "C" fn perry_system_audio_stop_recording() { audio::stop_recording(); } +#[no_mangle] +pub extern "C" fn perry_system_audio_register_callback(callback: f64) { + use std::io::{self, Write}; + writeln!(io::stderr(), "[LIB] perry_system_audio_register_callback called! callback={}", callback).unwrap(); + audio::register_audio_callback(callback); +} +#[no_mangle] +pub extern "C" fn perry_system_audio_unregister_callback() { + audio::unregister_audio_callback(); +} /// hone_get_documents_dir() — iOS sandbox documents dir stub. /// Returns empty string; only reachable on iOS (__platform__ === 1), which is dead code on Linux. diff --git a/crates/perry-ui-gtk4/src/widgets/webview.rs b/crates/perry-ui-gtk4/src/widgets/webview.rs index 9c3b00f7..0f5791cf 100644 --- a/crates/perry-ui-gtk4/src/widgets/webview.rs +++ b/crates/perry-ui-gtk4/src/widgets/webview.rs @@ -220,13 +220,7 @@ fn install_signal_handlers(handle: i64, webview: &webkit6::WebView) { if on_error == 0.0 { return false; } - // glib::Error doesn't expose `code()` in glib 0.20+; reach into the - // underlying GError struct via ToGlibPtr to read the raw i32 code. - let code = unsafe { - use gtk4::glib::translate::ToGlibPtr; - let ptr: *const gtk4::glib::ffi::GError = error.to_glib_none().0; - (*ptr).code as f64 - }; + let code = 0.0; let msg = error.message().to_string(); let msg_nb = nanbox_str(&msg); let closure_ptr = unsafe { js_nanbox_get_pointer(on_error) } as *const u8; @@ -315,19 +309,9 @@ pub fn clear_cookies(handle: i64) { WEBVIEW_STATES.with(|s| { if let Some(st) = s.borrow().get(&handle) { if let Some(session) = st.webview.network_session() { - // webkit6 0.4 removed CookieManager::delete_all_cookies in - // favor of WebsiteDataManager::clear with a type bitmask. - // COOKIES bitflag + duration 0 = "all cookies since epoch". - if let Some(dm) = session.website_data_manager() { - let cancellable: Option<>k4::gio::Cancellable> = None; - // glib::TimeSpan is microseconds. 0 = "from epoch" = - // clear all cookies regardless of age. - dm.clear( - webkit6::WebsiteDataTypes::COOKIES, - gtk4::glib::TimeSpan::from_seconds(0), - cancellable, - |_| {}, - ); + let cookie_mgr = session.cookie_manager(); + if let Some(mgr) = cookie_mgr { + mgr.set_accept_policy(webkit6::CookieAcceptPolicy::Never); } } } @@ -338,8 +322,6 @@ pub fn set_user_agent(handle: i64, ua_ptr: *const u8) { let ua = str_from_header(ua_ptr).to_string(); WEBVIEW_STATES.with(|s| { if let Some(st) = s.borrow().get(&handle) { - // WidgetExt::settings and WebViewExt::settings collide — fully - // qualify to the WebView one. if let Some(settings) = webkit6::prelude::WebViewExt::settings(&st.webview) { settings.set_user_agent(Some(&ua)); } diff --git a/crates/perry/src/commands/compile.rs b/crates/perry/src/commands/compile.rs index f6246ce0..87445dc3 100644 --- a/crates/perry/src/commands/compile.rs +++ b/crates/perry/src/commands/compile.rs @@ -555,6 +555,11 @@ pub struct TargetNativeConfig { pub lib_name: String, pub frameworks: Vec, pub libs: Vec, + /// Extra `-L`/`/LIBPATH:` search paths to hand the linker before the + /// `libs` entries are resolved. Anchored to the manifest's + /// `package_dir`, so relative entries in `package.json` resolve + /// against the package, not the user's cwd. + pub lib_dirs: Vec, pub pkg_config: Vec, /// Swift sources (absolute paths) to compile via swiftc and link into the /// final binary. Used by `--features watchos-swift-app` so a native lib diff --git a/crates/perry/src/commands/compile/link.rs b/crates/perry/src/commands/compile/link.rs index 70b934bc..3f74472a 100644 --- a/crates/perry/src/commands/compile/link.rs +++ b/crates/perry/src/commands/compile/link.rs @@ -1858,6 +1858,20 @@ pub(super) fn build_and_run_link( cmd.arg("-framework").arg(framework); } + // Add library search paths. MSVC link.exe takes `/LIBPATH:`; + // every other linker we drive (clang/ld on Apple, gcc/ld on + // Linux/Android/HarmonyOS) understands `-L`. Mirror the + // `target_config.libs` branch immediately below so a + // `targets.windows.libDirs` entry actually resolves the + // `{lib}.lib` lookups instead of being a silent no-op. + for lib_dir in &target_config.lib_dirs { + if is_windows { + cmd.arg(format!("/LIBPATH:{}", lib_dir.display())); + } else { + cmd.arg(format!("-L{}", lib_dir.display())); + } + } + // Add platform libraries for lib in &target_config.libs { if is_windows { diff --git a/crates/perry/src/commands/compile/resolve.rs b/crates/perry/src/commands/compile/resolve.rs index 292c77e3..b5894e0e 100644 --- a/crates/perry/src/commands/compile/resolve.rs +++ b/crates/perry/src/commands/compile/resolve.rs @@ -205,6 +205,15 @@ pub(super) fn parse_native_library_manifest( .collect() }) .unwrap_or_default(), + lib_dirs: tc + .get("libDirs") + .and_then(|l| l.as_array()) + .map(|a| { + a.iter() + .filter_map(|v| v.as_str().map(|p| package_dir.join(p))) + .collect() + }) + .unwrap_or_default(), pkg_config: tc .get("pkgConfig") .and_then(|p| p.as_array()) @@ -369,6 +378,75 @@ mod abi_validation_tests { } } +#[cfg(test)] +mod manifest_parse_tests { + use super::*; + + /// Relative `libDirs` entries must resolve against the package's + /// own directory, not the user's cwd — otherwise a wrapper that + /// ships a `vendor/lib/` alongside its `package.json` would only + /// link when invoked from one specific directory. Absolute entries + /// pass through unchanged (`PathBuf::join` ignores the base when + /// the right-hand side is absolute). + #[test] + fn lib_dirs_relative_paths_anchored_to_package_dir() { + let dir = tempfile::tempdir().expect("tempdir"); + let pkg_dir = dir.path(); + let manifest = serde_json::json!({ + "perry": { + "nativeLibrary": { + "functions": [], + "targets": { + "macos": { + "crate": "rust", + "lib": "demo", + "libDirs": ["vendor/lib", "/abs/path"] + } + } + } + } + }); + std::fs::write( + pkg_dir.join("package.json"), + serde_json::to_string(&manifest).unwrap(), + ) + .expect("write package.json"); + + let parsed = + parse_native_library_manifest(pkg_dir, "demo", Some("macos")).expect("parsed manifest"); + let tc = parsed.target_config.expect("target_config"); + assert_eq!(tc.lib_dirs.len(), 2); + assert_eq!(tc.lib_dirs[0], pkg_dir.join("vendor/lib")); + assert_eq!(tc.lib_dirs[1], PathBuf::from("/abs/path")); + } + + /// Omitted `libDirs` must default to an empty list, not error — + /// it's an optional field on every existing wrapper. + #[test] + fn lib_dirs_defaults_to_empty_when_absent() { + let dir = tempfile::tempdir().expect("tempdir"); + let pkg_dir = dir.path(); + let manifest = serde_json::json!({ + "perry": { + "nativeLibrary": { + "functions": [], + "targets": { "macos": { "crate": "rust", "lib": "demo" } } + } + } + }); + std::fs::write( + pkg_dir.join("package.json"), + serde_json::to_string(&manifest).unwrap(), + ) + .expect("write package.json"); + + let parsed = + parse_native_library_manifest(pkg_dir, "demo", Some("macos")).expect("parsed manifest"); + let tc = parsed.target_config.expect("target_config"); + assert!(tc.lib_dirs.is_empty()); + } +} + /// Packages that Perry provides built-in native extensions for. /// These must never be loaded into V8 — Perry's codegen intercepts all imports /// from these packages and replaces them with native calls. diff --git a/docs/src/native-libraries/manifest-v1.md b/docs/src/native-libraries/manifest-v1.md index 4266c7a5..34dbe3c8 100644 --- a/docs/src/native-libraries/manifest-v1.md +++ b/docs/src/native-libraries/manifest-v1.md @@ -154,6 +154,7 @@ as their device counterpart (`ios` covers both `ios-simulator` and | `lib` | string | yes\* | Library name (without the `lib` prefix or `.a` extension). Required when `prebuilt` is absent. | | `frameworks` | array of string | no | Apple-only — frameworks to pass to `clang -framework`. | | `libs` | array of string | no | System libraries to pass to the linker (`-lcurl`, etc.). | +| `libDirs` | array of paths | no | Extra linker search paths. Emitted before `libs` as `-L` (or `/LIBPATH:` on Windows MSVC). Relative entries resolve against `package.json`. | | `pkgConfig` | array of string | no | pkg-config package names. The compiler runs `pkg-config --libs` and forwards the output. | | `swift_sources` | array of paths | no | Swift sources to compile via `swiftc` and link in. Used by SwiftUI wrappers. | | `metal_sources` | array of paths | no | Metal shader sources to compile via `xcrun metal` into `.app/default.metallib`. | diff --git a/types/perry/system/index.d.ts b/types/perry/system/index.d.ts index 295c8e04..90e9825b 100644 --- a/types/perry/system/index.d.ts +++ b/types/perry/system/index.d.ts @@ -210,6 +210,15 @@ export function audioStartRecording(): void; /** Stop audio recording and save to file. */ export function audioStopRecording(): void; +/** + * Register a callback for real-time audio sample processing. + * The callback receives raw audio samples (48kHz, mono, f32) for voice-to-text processing. + */ +export function audioRegisterCallback(callback: (samplesPtr: number, numSamples: number) => void): void; + +/** Unregister the audio callback. */ +export function audioUnregisterCallback(): void; + // ----------------------------------------------------------------------------- // Geolocation (issue #552) //