Close Menu
geekfence.comgeekfence.com
    What's Hot

    IKS Health acquires TruBridge: financial engineering, value play, or a disruptive bet on rural healthcare?

    May 19, 2026

    Durable Standards Worth Institutional Investment – O’Reilly

    May 19, 2026

    Understanding the modern cybercrime landscape

    May 19, 2026
    Facebook X (Twitter) Instagram
    • About Us
    • Contact Us
    Facebook Instagram
    geekfence.comgeekfence.com
    • Home
    • UK Tech News
    • AI
    • Big Data
    • Cyber Security
      • Cloud Computing
      • iOS Development
    • IoT
    • Mobile
    • Software
      • Software Development
      • Software Engineering
    • Technology
      • Green Technology
      • Nanotechnology
    • Telecom
    geekfence.comgeekfence.com
    Home»iOS Development»Look What JavaScriptCore Has Been Doing in My Pocket
    iOS Development

    Look What JavaScriptCore Has Been Doing in My Pocket

    AdminBy AdminMay 19, 2026No Comments9 Mins Read3 Views
    Facebook Twitter Pinterest LinkedIn Telegram Tumblr Email
    Look What JavaScriptCore Has Been Doing in My Pocket
    Share
    Facebook Twitter LinkedIn Pinterest Email


    I’ll be honest. When I started thinking about which other languages SwiftBash should run, JavaScript was about fifth on my list. I’m a Swift person. I’m a Cocoa person. I’m somewhere between indifferent and faintly hostile to npm. The idea of “let’s drop a Node-compatible runtime into the bash shell” sounded exactly like the kind of project I would shake my head at on someone else’s GitHub.

    But it kept nagging. After SwiftScript and SwiftPorts, the obvious next move was another scripting language. And when I started enumerating them out loud — Python, Ruby, Lua, Perl, JavaScript — there was exactly one of those that Apple ships a complete, JIT-tuned interpreter for, on every platform, right out of the box.

    $ ls /System/Library/Frameworks | grep JavaScriptCore
    JavaScriptCore.framework

    So I went to look at what was actually in there. And from there it was a slow accumulation of small surprises that eventually had me writing a blog post that I was pretty sure I was never going to write.

    Surprise one: the engine is right there

    I knew JavaScriptCore existed. I’d seen it linked from WebKit-shaped places. I had a vague memory of it powering the JS in Safari content blockers. What I hadn’t quite registered was that the Swift bindings for it have been sitting in the SDK since iOS 7, that they’re three lines, and that they actually work:

    import JavaScriptCore
    
    let ctx = JSContext()!
    let result = ctx.evaluateScript("1 + 2 + 3")
    print(result?.toInt32() ?? -1)
    // 6

    That’s the entire engine. No external dependencies, no package manager, no build script. Same engine Safari uses. Available on every Apple device I own.

    OK, fine. Adding numbers in JavaScript is not a feature.

    Surprise two: the bridging is honest

    I wrote a tiny console.log:

    let log: @convention(block) (String) -> Void = { msg in
        print("[js]", msg)
    }
    ctx.setObject(log, forKeyedSubscript: "log" as NSString)
    ctx.evaluateScript("log('hello from JavaScript')")
    // [js] hello from JavaScript

    And then I sat there for a minute, because what just happened is that a JavaScript program called a Swift closure. There was no IPC. No serialisation. No JSON.stringify. The closure captured normally, the JS context handed it a String, the Swift code printed. They are the same process. They are sharing memory.

    And it goes both ways. JS can hand objects back to Swift, JS can build dictionaries that come out as [String: Any], Swift can hold a JSValue reference and call into it later. The bridge is so quiet you have to keep reminding yourself there’s a bridge there at all.

    I dimly remembered that this is, more or less, exactly how React Native works. So I went to check.

    Surprise three: this is a Whole Pattern

    When React Native shipped in 2015, the iOS app was a thin native shell. The actual app — the views, the state, the buttons that say ‘Buy’ — was JavaScript code that ran inside a JavaScriptCore context that the shell embedded. Same trick I’d just done in ten lines of Swift, except scaled up to be the substrate of half the App Store.

    Then I noticed Microsoft CodePush (now mostly succeeded by Expo’s EAS Update), which exists for one reason: if your iOS app’s logic is JavaScript, you can replace the JavaScript over the air, without an App Store review, because Apple’s clause 3.3.2 specifically blesses interpreted code. The native shell is fixed. The interpreted code can change.

    This was a quiet thing to discover. I had been thinking of “download a binary plugin and run it” as something iOS just doesn’t allow. And it doesn’t, if “binary” means machine code. But “download a JavaScript file and feed it to JSC” is — and has been for a decade — the documented, sanctioned way to ship live code to a sandboxed app on iOS. Discord does it. Shopify does it. Coinbase does it. The official JavaScript for Automation, the one you get with osascript -l JavaScript, does it. Scriptable on iOS is essentially a whole shell-environment-in-an-app that lives entirely on top of this same primitive.

    So somewhere between “let me try this thing” and “wait, this is the entire React Native business model”, my opinion of the project shifted from “amusing weekend toy” to “actually, why shouldn’t SwiftBash be able to run JavaScript?”

    Surprise four: you can re-emulate Node from inside

    Here’s where it got fun. JavaScriptCore is just the language — no console, no process, no fs. JS scripts written for real-world use don’t talk to “the language”, they talk to Node’s API surface: console.log, process.argv, require('fs').readFileSync(...), fetch, setTimeout.

    Which means: anything Node calls a “module” is just a string of JavaScript that has access to functions a runtime exposed. And we have a bridge for exposing functions.

    So the recipe is mechanical:

    let readFileSync: @convention(block) (String) -> String = { path in
        (try? String(contentsOfFile: path, encoding: .utf8)) ?? ""
    }
    let fs = JSValue(newObjectIn: ctx)!
    fs.setObject(readFileSync, forKeyedSubscript: "readFileSync" as NSString)
    ctx.setObject(fs, forKeyedSubscript: "fs" as NSString)

    …and now JavaScript can:

    console.log(fs.readFileSync('/etc/hosts').split('\n').length);

    You repeat that for console, for process, for path, for os, for crypto (Apple gives you CryptoKit), for zlib (the host has libz), for fetch (URLSession), for timers (DispatchSourceTimer). Each one is fifty to a hundred lines. After about a thousand lines of this kind of plumbing, you have a runtime where existing Node CLI scripts run completely unchanged:

    #!/usr/bin/env node
    const fs = require('node:fs');
    const args = process.argv.slice(2);
    const greeting = process.env.GREETING ?? 'Hello';
    console.log(`${greeting}, ${args[0] ?? process.env.USER}!`);

    That’s a script anyone might write. It uses require, process.argv, process.env, console.log. Drop it on disk, chmod +x, run. Same source on the desktop, same source on my iPad embedded inside an app, same source under the real node. The shebang says node, and as long as the binary that env finds first is ours, the script doesn’t know or care which engine just ran it. (The trick to make our binary shadow node is mildly amusing — argv[0] dispatch and a swift-js install subcommand that lays down symlinks for node and bun — but it’s not the interesting part.)

    Surprise five: Swift Tasks make child_process weird

    This was the part I genuinely did not see coming.

    Existing JavaScript scripts use child_process.execSync and friends, because that’s how you call out to git/grep/curl from Node. The naïve port forks /bin/sh, same way node does, and we’re back to “needs a Unix process model”. Which I cannot have on iOS.

    But I have something node and bun don’t: I have BashInterpreter sitting next to the JS engine in the same Swift process. SwiftBash already knows how to run printf | grep | wc -l without forking — every command is a registered Swift type, the pipeline is AsyncStream between them. So when a JavaScript program does

    require('node:child_process').execSync('printf "alpha\\nbeta\\ngamma\\n" | grep a | wc -l');
    //   → 3

    …the JS engine calls into a Swift bridge, which hands the string to a fresh BashInterpreter.Shell, which runs the pipeline as ordinary AsyncStream channels, and the JS gets "3\n" back. There is no fork. There is no /bin/sh. printf, grep, and wc all live as Swift commands inside this same process.

    I think the moment I really fell for this project was when I realised JS could “spawn” twenty concurrent bash pipelines:

    await Promise.all(
      Array.from({length: 20}, () => cp.exec('echo something'))
    );

    …in two milliseconds. Not because the engine is fast (node is fast too) but because there are no twenty processes involved. There are twenty Task.detached running twenty BashInterpreter.Shell instances on the same thread pool. Swift’s structured concurrency is the right primitive when your “child process” is a value type. It feels like a quiet violation of the laws of POSIX, in a good way.

    I have benchmarks somewhere that show this scaling cleanly to hundreds of concurrent in-process pipelines, where node and bun are bottlenecked on fork. But the thing I want to sit with is just the conceptual frame: a JavaScript program that thinks it’s spawning subprocesses, where every “process” is actually a Swift Task, and the entire thing runs inside one sandboxed app.

    Where this leaves me

    I started this with a flat skeptical “JavaScript? really?” and a vague sense that it would be a project I’d start, get bored with, and abandon. What I have instead is a thing that lets a JS shebang script run on macOS, iOS, the iPad, in a sandboxed app, and inside SwiftBash, with the same source. That can pipe through bash commands without spawning. That can be downloaded over the air the way React Native bundles have been for a decade. That is faster than node on cold start, smaller than node on disk, and surprisingly close to node on actual scripts.

    The honest takeaway, the one I keep coming back to: I had been treating JavaScriptCore the way you treat the /System/Library/Frameworks folder in general — as infrastructure for someone else’s app. It isn’t. It’s a fully-tuned scripting engine that has been sitting on every device I’ve ever owned, with first-class Swift bindings, explicitly blessed by Apple for executing untrusted / downloaded code, and almost nobody outside the React Native crowd seems to use it. That’s a strange situation. It feels like leaving money on the table.

    The repo is at Cocoanetics/SwiftBash. The full SwiftJS write-up — every layer, every cross-runtime parity test, the multi-call-binary trick, the --sandbox-env flag, the streaming spawn() follow-up — lives in Docs/SwiftJS.md. The swift-js install command will drop node/bun symlinks into a directory of your choice, so you can try running an existing Node script under it without changing anything.

    I’m especially curious whether anyone reading this has an iOS app where they’d want to ship downloadable JS as behaviour-on-demand. That’s the use case I have not yet gotten to play with, and it’s the one that turns this from a “fun shebang interpreter” into something with actual product shape. Open an issue on the repo, or write to me, and I’ll have Opus take a look at your script.

    Like this:

    Like Loading…

    Related


    Categories: Updates



    Source link

    Share. Facebook Twitter Pinterest LinkedIn Tumblr Email

    Related Posts

    ClockKit complications cheatsheet – The.Swift.Dev.

    May 18, 2026

    ios – How to achieve a 2 column grid of cards that expand into a full screen view when tapped upon?

    May 17, 2026

    My Outer Loop | Cocoanetics

    May 13, 2026

    Conventions for Xcode – The.Swift.Dev.

    May 12, 2026

    ios – How to add shadow to border?

    May 11, 2026

    Introducing SwiftPorts | Cocoanetics

    May 7, 2026
    Top Posts

    Understanding U-Net Architecture in Deep Learning

    November 25, 202540 Views

    Hard-braking events as indicators of road segment crash risk

    January 14, 202628 Views

    Redefining AI efficiency with extreme compression

    March 25, 202627 Views
    Don't Miss

    IKS Health acquires TruBridge: financial engineering, value play, or a disruptive bet on rural healthcare?

    May 19, 2026

    When IKS Health announced its agreement to acquire TruBridge on April 23, 2026, the market…

    Durable Standards Worth Institutional Investment – O’Reilly

    May 19, 2026

    Understanding the modern cybercrime landscape

    May 19, 2026

    How Data-Driven Journalists Are Using API News Apps to Improve Reporting

    May 19, 2026
    Stay In Touch
    • Facebook
    • Instagram
    About Us

    At GeekFence, we are a team of tech-enthusiasts, industry watchers and content creators who believe that technology isn’t just about gadgets—it’s about how innovation transforms our lives, work and society. We’ve come together to build a place where readers, thinkers and industry insiders can converge to explore what’s next in tech.

    Our Picks

    IKS Health acquires TruBridge: financial engineering, value play, or a disruptive bet on rural healthcare?

    May 19, 2026

    Durable Standards Worth Institutional Investment – O’Reilly

    May 19, 2026

    Subscribe to Updates

    Please enable JavaScript in your browser to complete this form.
    Loading
    • About Us
    • Contact Us
    • Disclaimer
    • Privacy Policy
    • Terms and Conditions
    © 2026 Geekfence.All Rigt Reserved.

    Type above and press Enter to search. Press Esc to cancel.