This guide explains how the iOS side of the plugin works and gives step-by-step instructions for implementing or updating methods on iOS, including updating the native llama.cpp layer (e.g. for vision model support) using the bootstrap script.
┌─────────────────────────────────────────────────────────────────┐
│ JavaScript (Capacitor app) │
│ LlamaCpp.initContext({ ... }), context.completion({ ... }) │
└───────────────────────────────┬─────────────────────────────────┘
│ Capacitor bridge
▼
┌─────────────────────────────────────────────────────────────────┐
│ LlamaCppPlugin.swift (Capacitor plugin) │
│ - Declares plugin methods (CAPPluginMethod) │
│ - Reads args from CAPPluginCall, calls implementation │
│ - Resolves/rejects with JSObject │
└───────────────────────────────┬─────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ LlamaCpp.swift (implementation) │
│ - Holds contexts, loads native framework via dlsym │
│ - Calls C function pointers (initContext, completion, etc.) │
└───────────────────────────────┬─────────────────────────────────┘
│ dlsym → C symbols
▼
┌─────────────────────────────────────────────────────────────────┐
│ llama-cpp.framework (native C/C++) │
│ - Built from cpp/ (cap-llama.cpp, cap-completion.cpp, etc.) │
│ - Exposes C API used by Swift (e.g. llama_init_context) │
└─────────────────────────────────────────────────────────────────┘
- LlamaCppPlugin.swift: Thin bridge; every API is a
@PluginMethodthat forwards toLlamaCpp. - LlamaCpp.swift: Loads
llama-cpp.framework, resolves C symbols withdlsym, keeps context map, implements init/completion/embedding/etc. - Native layer: Built from
cpp/by CMake intoios/build/llama-cpp.framework, then copied toios/Frameworks/. The C entry points called from Swift must be exported from this framework.
To add or change a single method end-to-end on iOS:
The Swift code calls into the framework via C function pointers (see LlamaCpp.swift: loadFunctionPointers(), initContextFunc, completionFunc, etc.). So the native (C/C++) code must expose a C symbol.
- If the symbol already exists (e.g.
llama_init_context): skip to Step 2. - If you need a new C entry point (e.g. for vision):
- Add or update the C/C++ implementation in
cpp/(e.g. incap-llama.cppor a newcap-vision.cpp). - Declare a C-compatible function in a header or in a
.cppwithextern "C"and ensure it’s linked into the iOS framework (it’s part of the CMake target inios/CMakeLists.txt). - Rebuild the framework (Step 5 below) and optionally verify with
nm ios/Frameworks/llama-cpp.framework/llama-cpp | grep your_symbol.
- Add or update the C/C++ implementation in
- Add a function pointer variable (e.g.
private var myNewFunc: ((Int64, String) -> String?)?). - In
loadFunctionPointers(), add:myNewFunc = unsafeBitCast(dlsym(library, "my_new_func"), to: ((Int64, String) -> String?).self)
- In the implementation method that uses it, guard on
myNewFunc != niland call it.
- Add a method on
LlamaCpp, e.g.func myNewFeature(contextId: Int, input: String, completion: @escaping (LlamaResult<...>) -> Void). - Look up context, optionally call
loadFunctionPointers(), call the native function pointer, convert result to a dictionary/type that matches the plugin API, and callcompletion(.success(...))orcompletion(.failure(...)).
- In
pluginMethods, add:CAPPluginMethod(name: "myNewFeature", returnType: CAPPluginReturnPromise).
- Add an
@objc func myNewFeature(_ call: CAPPluginCall)that:- Reads arguments from
call(e.g.call.getInt("contextId"),call.getString("input")). - Calls
implementation.myNewFeature(...) { result in ... }. - On success:
call.resolve(...); on failure:call.reject(...).
- Reads arguments from
From the project root:
# Option A: build script (recommended)
./build-native.sh
# Option B: iOS only
cd ios/build && cmake .. -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=13.0 && cmake --build . --config ReleaseThen copy the built framework to ios/Frameworks/ if your script doesn’t do it (the root build-native.sh does).
If this is a new plugin method, add the corresponding method to src/definitions.ts and implement or stub it in src/index.ts and src/web.ts so the JS API is consistent across platforms.
The native source under cpp/ is based on ggerganov/llama.cpp. To pull in a newer version (e.g. with vision model support) without losing the Capacitor adapter code, use the bootstrap script.
- Clones upstream
llama.cpp(default:master; you can pass a branch, tag, or commit). - Syncs upstream files into
cpp/, excluding project-specific files so they are not overwritten.
Never overwritten:
cap-llama.cpp/cap-llama.hcap-completion.cpp/cap-completion.hcap-tts.cpp/cap-tts.hcap-embedding.cpp/cap-embedding.hcap-mtmd.hppcpp/README.mdcpp/tools/mtmd/(multimodal/vision tooling)
Everything else in cpp/ (ggml, llama, common, chat, etc.) is updated from upstream.
-
Run the bootstrap script (from repo root):
./scripts/bootstrap.sh [REF]
REFoptional: branch, tag, or commit (default:master).- Example:
./scripts/bootstrap.sh master - Example:
./scripts/bootstrap.sh b5234(specific commit)
-
Resolve conflicts
If you had local changes in non–project-specific files, fix any merge/conflict markers left incpp/. -
Reconcile adapter code with upstream
Upstream APIs (function names, structs, init flags) sometimes change. You may need to update:cap-llama.cpp/cap-llama.h(context init, model load)cap-completion.cpp(completion loop)cap-tts.cppif TTS API changedcap-embedding.cppif embedding API changedcap-mtmd.hppandcpp/tools/mtmd/for vision/multimodal: align with any new llama.cpp multimodal/vision APIs (e.g. clip, llava, or new projector types).
-
Rebuild native libraries
npm run build:native # or ./build-native.sh -
Verify iOS
Open the app in Xcode, run on device/simulator, and test init, completion, and (if applicable) vision/multimodal flows.
- Current state: The plugin’s native layer may not support the latest vision models (e.g. newer LLaVA or other vision architectures) until the native code is updated.
- How to get vision support:
- Run
./scripts/bootstrap.shwith a ref that includes the vision changes you need (e.g.masteror a specific tag). - Keep
cap-*.cpp/handcpp/tools/mtmd/; update them to use the new upstream APIs (e.g. new init flags, new clip/vision entry points). - If Swift currently calls a C symbol that was renamed or removed, update
LlamaCpp.swift(function pointer name and/or signature) and repeat the “implement a method” steps above. - Rebuild with
./build-native.shand test on device.
- Run
| Layer | File(s) | Role |
|---|---|---|
| Plugin | ios/Sources/LlamaCppPlugin/LlamaCppPlugin.swift |
Capacitor plugin; declares methods, forwards to LlamaCpp. |
| Implementation | ios/Sources/LlamaCppPlugin/LlamaCpp.swift |
Loads framework, dlsym C symbols, implements logic. |
| Native | cpp/cap-llama.cpp, cap-completion.cpp, cap-tts.cpp, … |
C++ bridge used by iOS/Android builds. |
| Build | ios/CMakeLists.txt, build-native.sh |
Builds llama-cpp.framework from cpp/. |
| Bootstrap | scripts/bootstrap.sh |
Syncs upstream llama.cpp into cpp/ while keeping adapter code. |
- Run
./scripts/bootstrap.sh [ref]. - Manually update
cap-*.cpp/cap-mtmd.hpp/tools/mtmd/to match new upstream APIs. - Rebuild:
./build-native.sh. - If you added/renamed C symbols used by iOS, update
LlamaCpp.swift(function pointers and implementation). - If you added new plugin APIs, update
LlamaCppPlugin.swift,definitions.ts,index.ts, andweb.ts. - Test on iOS device/simulator.
For more detail on the native layer and what’s project-specific, see cpp/README.md.