1 // Copyright 2026 Splitfire AB (Onde Inference). All rights reserved.
2 // SPDX-License-Identifier: MIT OR Apache-2.0
3
4 //! C FFI bridge for the Onde React Native npm SDK.
5 //!
6 //! This crate wraps [`onde::inference::engine::ChatEngine`] with `extern "C"`
7 //! functions callable from Swift (iOS) and Kotlin/JNI (Android).
8 //!
9 //! ## Design
10 //!
11 //! - **Opaque pointer** (`*mut c_void`) for the engine handle.
12 //! - **JSON strings** (via `serde_json`) for complex types crossing FFI.
13 //! - **Global `tokio::Runtime`** created once via `OnceLock`.
14 //! - Every function that returns a `*mut c_char` requires the caller to free
15 //! it with [`onde_free_string`].
16 //! - Android JNI wrappers are gated behind `#[cfg(target_os = "android")]`.
17
18 use std::ffi::{c_char, c_void, CStr, CString};
19 use std::sync::OnceLock;
20
21 use serde::de::DeserializeOwned;
22 use serde::Serialize;
23 use tokio::runtime::Runtime;
24
25 use onde::inference::engine::ChatEngine;
26 use onde::inference::types::{ChatMessage, GgufModelConfig, SamplingConfig, UqffModelConfig};
27
28 // ── Global Tokio runtime ─────────────────────────────────────────────────────
29
30 /// Returns a reference to the global multi-threaded Tokio runtime.
31 ///
32 /// The runtime is created on first access and lives for the process lifetime.
33 fn runtime() -> &'static Runtime {
34 static RT: OnceLock<Runtime> = OnceLock::new();
35 RT.get_or_init(|| {
36 tokio::runtime::Builder::new_multi_thread()
37 .enable_all()
38 .build()
39 .expect("onde-react-native: failed to create tokio runtime")
40 })
41 }
42
43 // ── Helper functions ─────────────────────────────────────────────────────────
44
45 /// Serialize any `Serialize` value to a heap-allocated C string.
46 ///
47 /// The caller **must** free the returned pointer with [`onde_free_string`].
48 fn to_json_cstring<T: Serialize>(value: &T) -> *mut c_char {
49 match serde_json::to_string(value) {
50 Ok(json) => CString::new(json).unwrap_or_default().into_raw(),
51 Err(err) => error_json(&format!("JSON serialization failed: {err}")),
52 }
53 }
54
55 /// Deserialize a JSON C string into a Rust type.
56 ///
57 /// Returns `None` if the pointer is null or the JSON is malformed.
58 fn from_json_cstr<T: DeserializeOwned>(ptr: *const c_char) -> Option<T> {
59 if ptr.is_null() {
60 return None;
61 }
62 let cstr = unsafe { CStr::from_ptr(ptr) };
63 let s = cstr.to_str().ok()?;
64 serde_json::from_str(s).ok()
65 }
66
67 /// Read an optional C string parameter into an `Option<String>`.
68 ///
69 /// Returns `None` if the pointer is null or the string is empty.
70 fn nullable_str(ptr: *const c_char) -> Option<String> {
71 if ptr.is_null() {
72 return None;
73 }
74 let cstr = unsafe { CStr::from_ptr(ptr) };
75 let s = cstr.to_str().ok()?;
76 if s.is_empty() {
77 None
78 } else {
79 Some(s.to_owned())
80 }
81 }
82
83 /// Create a heap-allocated JSON error string: `{ "error": "<msg>" }`.
84 fn error_json(msg: &str) -> *mut c_char {
85 let json = serde_json::json!({ "error": msg });
86 CString::new(json.to_string())
87 .unwrap_or_default()
88 .into_raw()
89 }
90
91 // ── Engine lifecycle ─────────────────────────────────────────────────────────
92
93 /// Create a new `ChatEngine` and return an opaque pointer to it.
94 ///
95 /// The engine starts with no model loaded. The caller must eventually call
96 /// [`onde_engine_destroy`] to free the memory.
97 #[no_mangle]
98 pub extern "C" fn onde_engine_create() -> *mut c_void {
99 let engine = Box::new(ChatEngine::new());
100 Box::into_raw(engine) as *mut c_void
101 }
102
103 /// Destroy an engine previously created with [`onde_engine_create`].
104 ///
105 /// After this call the pointer is invalid and must not be used.
106 /// Passing a null pointer is a no-op.
107 #[no_mangle]
108 pub extern "C" fn onde_engine_destroy(engine: *mut c_void) {
109 if engine.is_null() {
110 return;
111 }
112 unsafe {
113 let _ = Box::from_raw(engine as *mut ChatEngine);
114 }
115 }
116
117 /// Load the platform-default GGUF model.
118 ///
119 /// # Parameters
120 ///
121 /// - `engine` — opaque engine pointer from [`onde_engine_create`].
122 /// - `system_prompt` — optional C string (pass null to omit).
123 /// - `sampling_json` — optional JSON-encoded `SamplingConfig` (pass null for defaults).
124 ///
125 /// # Returns
126 ///
127 /// A heap-allocated JSON C string (free with [`onde_free_string`]):
128 /// - On success: `{ "elapsed_secs": <f64> }`
129 /// - On failure: `{ "error": "<description>" }`
130 #[no_mangle]
131 pub extern "C" fn onde_engine_load_default_model(
132 engine: *mut c_void,
133 system_prompt: *const c_char,
134 sampling_json: *const c_char,
135 ) -> *mut c_char {
136 if engine.is_null() {
137 return error_json("engine pointer is null");
138 }
139 let engine_ref = unsafe { &*(engine as *const ChatEngine) };
140 let prompt = nullable_str(system_prompt);
141 let sampling: Option<SamplingConfig> = from_json_cstr(sampling_json);
142 let config = GgufModelConfig::platform_default();
143
144 runtime().block_on(async {
145 match engine_ref.load_gguf_model(config, prompt, sampling).await {
146 Ok(duration) => {
147 let result = serde_json::json!({ "elapsed_secs": duration.as_secs_f64() });
148 to_json_cstring(&result)
149 }
150 Err(err) => error_json(&err.to_string()),
151 }
152 })
153 }
154
155 /// Load a specific GGUF model from a JSON-encoded [`GgufModelConfig`].
156 ///
157 /// # Parameters
158 ///
159 /// - `config_json` — JSON-encoded `GgufModelConfig`.
160 /// - `system_prompt` — optional C string (pass null to omit).
161 /// - `sampling_json` — optional JSON-encoded `SamplingConfig` (pass null for defaults).
162 ///
163 /// # Returns
164 ///
165 /// Same JSON shape as [`onde_engine_load_default_model`].
166 #[no_mangle]
167 pub extern "C" fn onde_engine_load_model(
168 engine: *mut c_void,
169 config_json: *const c_char,
170 system_prompt: *const c_char,
171 sampling_json: *const c_char,
172 ) -> *mut c_char {
173 if engine.is_null() {
174 return error_json("engine pointer is null");
175 }
176 let engine_ref = unsafe { &*(engine as *const ChatEngine) };
177
178 let config: GgufModelConfig = match from_json_cstr(config_json) {
179 Some(c) => c,
180 None => return error_json("invalid or null config_json"),
181 };
182 let prompt = nullable_str(system_prompt);
183 let sampling: Option<SamplingConfig> = from_json_cstr(sampling_json);
184
185 runtime().block_on(async {
186 match engine_ref.load_gguf_model(config, prompt, sampling).await {
187 Ok(duration) => {
188 let result = serde_json::json!({ "elapsed_secs": duration.as_secs_f64() });
189 to_json_cstring(&result)
190 }
191 Err(err) => error_json(&err.to_string()),
192 }
193 })
194 }
195
196 /// Load a specific UQFF model from a JSON-encoded [`UqffModelConfig`].
197 ///
198 /// # Parameters
199 ///
200 /// - `config_json` — JSON-encoded `UqffModelConfig`.
201 /// - `system_prompt` — optional C string (pass null to omit).
202 /// - `sampling_json` — optional JSON-encoded `SamplingConfig` (pass null for defaults).
203 ///
204 /// # Returns
205 ///
206 /// Same JSON shape as [`onde_engine_load_default_model`].
207 #[no_mangle]
208 pub extern "C" fn onde_engine_load_uqff_model(
209 engine: *mut c_void,
210 config_json: *const c_char,
211 system_prompt: *const c_char,
212 sampling_json: *const c_char,
213 ) -> *mut c_char {
214 if engine.is_null() {
215 return error_json("engine pointer is null");
216 }
217 let engine_ref = unsafe { &*(engine as *const ChatEngine) };
218
219 let config: UqffModelConfig = match from_json_cstr(config_json) {
220 Some(c) => c,
221 None => return error_json("invalid or null config_json"),
222 };
223 let prompt = nullable_str(system_prompt);
224 let sampling: Option<SamplingConfig> = from_json_cstr(sampling_json);
225
226 runtime().block_on(async {
227 match engine_ref.load_uqff_model(config, prompt, sampling).await {
228 Ok(duration) => {
229 let result = serde_json::json!({ "elapsed_secs": duration.as_secs_f64() });
230 to_json_cstring(&result)
231 }
232 Err(err) => error_json(&err.to_string()),
233 }
234 })
235 }
236
237 /// Unload the currently loaded model.
238 ///
239 /// # Returns
240 ///
241 /// JSON: `{ "model_name": "<name>" }` or `{ "model_name": null }`.
242 #[no_mangle]
243 pub extern "C" fn onde_engine_unload_model(engine: *mut c_void) -> *mut c_char {
244 if engine.is_null() {
245 return error_json("engine pointer is null");
246 }
247 let engine_ref = unsafe { &*(engine as *const ChatEngine) };
248 let name = runtime().block_on(engine_ref.unload_model());
249 let result = serde_json::json!({ "model_name": name });
250 to_json_cstring(&result)
251 }
252
253 /// Check whether a model is currently loaded.
254 #[no_mangle]
255 pub extern "C" fn onde_engine_is_loaded(engine: *mut c_void) -> bool {
256 if engine.is_null() {
257 return false;
258 }
259 let engine_ref = unsafe { &*(engine as *const ChatEngine) };
260 runtime().block_on(engine_ref.is_loaded())
261 }
262
263 // ── Engine info ──────────────────────────────────────────────────────────────
264
265 /// Get a snapshot of the engine's current state as JSON.
266 ///
267 /// # Returns
268 ///
269 /// JSON-encoded [`EngineInfo`](onde::inference::types::EngineInfo).
270 #[no_mangle]
271 pub extern "C" fn onde_engine_info(engine: *mut c_void) -> *mut c_char {
272 if engine.is_null() {
273 return error_json("engine pointer is null");
274 }
275 let engine_ref = unsafe { &*(engine as *const ChatEngine) };
276 let info = runtime().block_on(engine_ref.info());
277 to_json_cstring(&info)
278 }
279
280 // ── System prompt ────────────────────────────────────────────────────────────
281
282 /// Set or replace the engine's system prompt.
283 #[no_mangle]
284 pub extern "C" fn onde_engine_set_system_prompt(engine: *mut c_void, prompt: *const c_char) {
285 if engine.is_null() {
286 return;
287 }
288 let engine_ref = unsafe { &*(engine as *const ChatEngine) };
289 if let Some(text) = nullable_str(prompt) {
290 runtime().block_on(engine_ref.set_system_prompt(text));
291 }
292 }
293
294 /// Clear the engine's system prompt.
295 #[no_mangle]
296 pub extern "C" fn onde_engine_clear_system_prompt(engine: *mut c_void) {
297 if engine.is_null() {
298 return;
299 }
300 let engine_ref = unsafe { &*(engine as *const ChatEngine) };
301 runtime().block_on(engine_ref.clear_system_prompt());
302 }
303
304 // ── Sampling ─────────────────────────────────────────────────────────────────
305
306 /// Replace the engine's sampling configuration from a JSON string.
307 #[no_mangle]
308 pub extern "C" fn onde_engine_set_sampling(engine: *mut c_void, sampling_json: *const c_char) {
309 if engine.is_null() {
310 return;
311 }
312 let engine_ref = unsafe { &*(engine as *const ChatEngine) };
313 if let Some(sampling) = from_json_cstr::<SamplingConfig>(sampling_json) {
314 runtime().block_on(engine_ref.set_sampling(sampling));
315 }
316 }
317
318 // ── History ──────────────────────────────────────────────────────────────────
319
320 /// Get the conversation history as a JSON array of `ChatMessage` objects.
321 #[no_mangle]
322 pub extern "C" fn onde_engine_history(engine: *mut c_void) -> *mut c_char {
323 if engine.is_null() {
324 return to_json_cstring(&serde_json::json!([]));
325 }
326 let engine_ref = unsafe { &*(engine as *const ChatEngine) };
327 let history = runtime().block_on(engine_ref.history());
328 to_json_cstring(&history)
329 }
330
331 /// Clear the conversation history. Returns the number of turns removed.
332 #[no_mangle]
333 pub extern "C" fn onde_engine_clear_history(engine: *mut c_void) -> u64 {
334 if engine.is_null() {
335 return 0;
336 }
337 let engine_ref = unsafe { &*(engine as *const ChatEngine) };
338 runtime().block_on(engine_ref.clear_history()) as u64
339 }
340
341 /// Append a message to the conversation history without running inference.
342 ///
343 /// `message_json` must be a JSON-encoded `ChatMessage`.
344 #[no_mangle]
345 pub extern "C" fn onde_engine_push_history(engine: *mut c_void, message_json: *const c_char) {
346 if engine.is_null() {
347 return;
348 }
349 let engine_ref = unsafe { &*(engine as *const ChatEngine) };
350 if let Some(message) = from_json_cstr::<ChatMessage>(message_json) {
351 runtime().block_on(engine_ref.push_history(message));
352 }
353 }
354
355 // ── Non-streaming inference ──────────────────────────────────────────────────
356
357 /// Send a user message and receive a complete assistant reply.
358 ///
359 /// The message and reply are appended to the conversation history.
360 ///
361 /// # Returns
362 ///
363 /// JSON-encoded [`InferenceResult`](onde::inference::types::InferenceResult)
364 /// on success, or `{ "error": "<description>" }` on failure.
365 #[no_mangle]
366 pub extern "C" fn onde_engine_send_message(
367 engine: *mut c_void,
368 message: *const c_char,
369 ) -> *mut c_char {
370 if engine.is_null() {
371 return error_json("engine pointer is null");
372 }
373 let engine_ref = unsafe { &*(engine as *const ChatEngine) };
374 let text = match nullable_str(message) {
375 Some(t) => t,
376 None => return error_json("message is null or empty"),
377 };
378
379 runtime().block_on(async {
380 match engine_ref.send_message(text).await {
381 Ok(result) => to_json_cstring(&result),
382 Err(err) => error_json(&err.to_string()),
383 }
384 })
385 }
386
387 /// One-shot generation without modifying conversation history.
388 ///
389 /// # Parameters
390 ///
391 /// - `messages_json` — JSON array of `ChatMessage`.
392 /// - `sampling_json` — optional JSON-encoded `SamplingConfig` (null for engine defaults).
393 ///
394 /// # Returns
395 ///
396 /// JSON-encoded [`InferenceResult`] or `{ "error": "..." }`.
397 #[no_mangle]
398 pub extern "C" fn onde_engine_generate(
399 engine: *mut c_void,
400 messages_json: *const c_char,
401 sampling_json: *const c_char,
402 ) -> *mut c_char {
403 if engine.is_null() {
404 return error_json("engine pointer is null");
405 }
406 let engine_ref = unsafe { &*(engine as *const ChatEngine) };
407
408 let messages: Vec<ChatMessage> = match from_json_cstr(messages_json) {
409 Some(m) => m,
410 None => return error_json("invalid or null messages_json"),
411 };
412 let sampling: Option<SamplingConfig> = from_json_cstr(sampling_json);
413
414 runtime().block_on(async {
415 match engine_ref.generate(messages, sampling).await {
416 Ok(result) => to_json_cstring(&result),
417 Err(err) => error_json(&err.to_string()),
418 }
419 })
420 }
421
422 // ── Streaming inference ──────────────────────────────────────────────────────
423
424 /// Send a user message and stream token chunks via a C callback.
425 ///
426 /// The callback is invoked for each chunk with:
427 /// - `chunk_json` — a JSON-encoded [`StreamChunk`](onde::inference::types::StreamChunk)
428 /// (valid only for the duration of the callback invocation).
429 /// - `done` — `true` when this is the final chunk.
430 ///
431 /// # Returns
432 ///
433 /// - `null` on success (streaming completed).
434 /// - A heap-allocated JSON error string on failure (free with [`onde_free_string`]).
435 #[no_mangle]
436 pub extern "C" fn onde_engine_stream_message(
437 engine: *mut c_void,
438 message: *const c_char,
439 callback: extern "C" fn(*const c_char, bool),
440 ) -> *mut c_char {
441 if engine.is_null() {
442 return error_json("engine pointer is null");
443 }
444 let engine_ref = unsafe { &*(engine as *const ChatEngine) };
445 let text = match nullable_str(message) {
446 Some(t) => t,
447 None => return error_json("message is null or empty"),
448 };
449
450 runtime().block_on(async {
451 let receiver = match engine_ref.stream_message(text).await {
452 Ok(rx) => rx,
453 Err(err) => return error_json(&err.to_string()),
454 };
455
456 // Drain the channel and forward each chunk through the callback.
457 let mut receiver = receiver;
458 loop {
459 match receiver.recv().await {
460 Some(chunk) => {
461 let done = chunk.done;
462 match serde_json::to_string(&chunk) {
463 Ok(json) => {
464 if let Ok(cstr) = CString::new(json) {
465 callback(cstr.as_ptr(), done);
466 }
467 }
468 Err(err) => {
469 log::error!(
470 "onde_engine_stream_message: JSON serialization failed: {err}"
471 );
472 }
473 }
474 if done {
475 break;
476 }
477 }
478 None => {
479 // Channel closed without a done chunk — send a synthetic one.
480 let final_json = serde_json::json!({
481 "delta": "",
482 "done": true,
483 "finish_reason": "channel_closed"
484 });
485 if let Ok(cstr) = CString::new(final_json.to_string()) {
486 callback(cstr.as_ptr(), true);
487 }
488 break;
489 }
490 }
491 }
492
493 std::ptr::null_mut()
494 })
495 }
496
497 // ── Model config presets ─────────────────────────────────────────────────────
498
499 /// Return the platform-default GGUF model config as JSON.
500 #[no_mangle]
501 pub extern "C" fn onde_default_model_config() -> *mut c_char {
502 to_json_cstring(&GgufModelConfig::platform_default())
503 }
504
505 /// Build a generic UQFF model config as JSON.
506 #[no_mangle]
507 pub extern "C" fn onde_uqff_model_config(
508 model_id: *const c_char,
509 files_json: *const c_char,
510 display_name: *const c_char,
511 approx_memory: *const c_char,
512 chat_template: *const c_char,
513 ) -> *mut c_char {
514 let model_id = match nullable_str(model_id) {
515 Some(value) => value,
516 None => return error_json("model_id is required"),
517 };
518 let files: Vec<String> = match from_json_cstr(files_json) {
519 Some(value) => value,
520 None => return error_json("invalid or null files_json"),
521 };
522 let display_name = match nullable_str(display_name) {
523 Some(value) => value,
524 None => return error_json("display_name is required"),
525 };
526 let approx_memory = match nullable_str(approx_memory) {
527 Some(value) => value,
528 None => return error_json("approx_memory is required"),
529 };
530
531 to_json_cstring(&UqffModelConfig {
532 model_id,
533 files,
534 display_name,
535 approx_memory,
536 chat_template: nullable_str(chat_template),
537 })
538 }
539
540 /// Return the Qwen 2.5 1.5B GGUF model config as JSON.
541 #[no_mangle]
542 pub extern "C" fn onde_qwen25_1_5b_config() -> *mut c_char {
543 to_json_cstring(&GgufModelConfig::qwen25_1_5b())
544 }
545
546 /// Return the Qwen 2.5 3B GGUF model config as JSON.
547 #[no_mangle]
548 pub extern "C" fn onde_qwen25_3b_config() -> *mut c_char {
549 to_json_cstring(&GgufModelConfig::qwen25_3b())
550 }
551
552 // ── Qwen 3 family ────────────────────────────────────────────────────────────
553
554 /// Return the Qwen 3 0.6B GGUF model config as JSON.
555 #[no_mangle]
556 pub extern "C" fn onde_qwen3_0_6b_config() -> *mut c_char {
557 to_json_cstring(&GgufModelConfig::qwen3_0_6b())
558 }
559
560 /// Return the Qwen 3 1.7B GGUF model config as JSON.
561 #[no_mangle]
562 pub extern "C" fn onde_qwen3_1_7b_config() -> *mut c_char {
563 to_json_cstring(&GgufModelConfig::qwen3_1_7b())
564 }
565
566 /// Return the Qwen 3 4B GGUF model config as JSON.
567 #[no_mangle]
568 pub extern "C" fn onde_qwen3_4b_config() -> *mut c_char {
569 to_json_cstring(&GgufModelConfig::qwen3_4b())
570 }
571
572 /// Return the Qwen 3 8B GGUF model config as JSON.
573 #[no_mangle]
574 pub extern "C" fn onde_qwen3_8b_config() -> *mut c_char {
575 to_json_cstring(&GgufModelConfig::qwen3_8b())
576 }
577
578 /// Return the Qwen 3 14B GGUF model config as JSON.
579 #[no_mangle]
580 pub extern "C" fn onde_qwen3_14b_config() -> *mut c_char {
581 to_json_cstring(&GgufModelConfig::qwen3_14b())
582 }
583
584 /// Return the Qwen 3 32B GGUF model config as JSON.
585 #[no_mangle]
586 pub extern "C" fn onde_qwen3_32b_config() -> *mut c_char {
587 to_json_cstring(&GgufModelConfig::qwen3_32b())
588 }
589
590 /// Return the Qwen 3 4B Instruct 2507 GGUF model config as JSON.
591 #[no_mangle]
592 pub extern "C" fn onde_qwen3_4b_instruct_2507_config() -> *mut c_char {
593 to_json_cstring(&GgufModelConfig::qwen3_4b_instruct_2507())
594 }
595
596 /// Return the Qwen 3 4B Thinking 2507 GGUF model config as JSON.
597 #[no_mangle]
598 pub extern "C" fn onde_qwen3_4b_thinking_2507_config() -> *mut c_char {
599 to_json_cstring(&GgufModelConfig::qwen3_4b_thinking_2507())
600 }
601
602 /// Return the Qwen 3 30B-A3B Instruct 2507 (MoE) GGUF model config as JSON.
603 #[no_mangle]
604 pub extern "C" fn onde_qwen3_30b_a3b_instruct_2507_config() -> *mut c_char {
605 to_json_cstring(&GgufModelConfig::qwen3_30b_a3b_instruct_2507())
606 }
607
608 // ── Sampling presets ─────────────────────────────────────────────────────────
609
610 /// Return the default sampling config as JSON.
611 #[no_mangle]
612 pub extern "C" fn onde_default_sampling_config() -> *mut c_char {
613 to_json_cstring(&SamplingConfig::default())
614 }
615
616 /// Return the deterministic (greedy) sampling config as JSON.
617 #[no_mangle]
618 pub extern "C" fn onde_deterministic_sampling_config() -> *mut c_char {
619 to_json_cstring(&SamplingConfig::deterministic())
620 }
621
622 /// Return the mobile sampling config as JSON.
623 #[no_mangle]
624 pub extern "C" fn onde_mobile_sampling_config() -> *mut c_char {
625 to_json_cstring(&SamplingConfig::mobile())
626 }
627
628 // ── Memory management ────────────────────────────────────────────────────────
629
630 /// Free a C string previously returned by any `onde_*` function.
631 ///
632 /// Passing a null pointer is a no-op.
633 #[no_mangle]
634 #[allow(clippy::not_unsafe_ptr_arg_deref)]
635 pub extern "C" fn onde_free_string(ptr: *mut c_char) {
636 if ptr.is_null() {
637 return;
638 }
639 unsafe {
640 let _ = CString::from_raw(ptr);
641 }
642 }
643
644 // ═══════════════════════════════════════════════════════════════════════════════
645 // Android JNI bindings
646 // ═══════════════════════════════════════════════════════════════════════════════
647
648 #[cfg(target_os = "android")]
649 mod android {
650 use jni::objects::{JClass, JString};
651 use jni::sys::{jboolean, jlong, jstring, JNI_FALSE, JNI_TRUE};
652 use jni::JNIEnv;
653
654 use std::ffi::{c_char, c_void, CStr, CString};
655
656 use super::*;
657
658 // ── JNI helpers ──────────────────────────────────────────────────────
659
660 /// Convert a JNI `JString` to a `CString`.
661 ///
662 /// Returns `None` if the JString is null or conversion fails.
663 /// The caller must keep the returned `CString` alive for as long as
664 /// the `.as_ptr()` result is used.
665 fn jstring_to_cstring(env: &mut JNIEnv, jstr: &JString) -> Option<CString> {
666 if jstr.is_null() {
667 return None;
668 }
669 let java_str = env.get_string(jstr).ok()?;
670 let rust_str: String = java_str.into();
671 CString::new(rust_str).ok()
672 }
673
674 /// Convert a `*mut c_char` (owned C string) to a JNI `jstring`.
675 ///
676 /// Frees the C string after conversion. Returns null jstring on failure.
677 fn cstring_ptr_to_jstring(env: &mut JNIEnv, ptr: *mut c_char) -> jstring {
678 if ptr.is_null() {
679 return std::ptr::null_mut();
680 }
681 let cstr = unsafe { CStr::from_ptr(ptr) };
682 let result = match cstr.to_str() {
683 Ok(s) => env
684 .new_string(s)
685 .map(|js| js.into_raw())
686 .unwrap_or(std::ptr::null_mut()),
687 Err(_) => std::ptr::null_mut(),
688 };
689 // Free the C string.
690 onde_free_string(ptr);
691 result
692 }
693
694 // ── Engine lifecycle ─────────────────────────────────────────────────
695
696 #[no_mangle]
697 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEngineCreate(
698 _env: JNIEnv,
699 _class: JClass,
700 ) -> jlong {
701 onde_engine_create() as jlong
702 }
703
704 #[no_mangle]
705 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEngineDestroy(
706 _env: JNIEnv,
707 _class: JClass,
708 engine: jlong,
709 ) {
710 onde_engine_destroy(engine as *mut c_void);
711 }
712
713 #[no_mangle]
714 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEngineLoadDefaultModel(
715 mut env: JNIEnv,
716 _class: JClass,
717 engine: jlong,
718 system_prompt: JString,
719 sampling_json: JString,
720 ) -> jstring {
721 let prompt_cstr = jstring_to_cstring(&mut env, &system_prompt);
722 let sampling_cstr = jstring_to_cstring(&mut env, &sampling_json);
723
724 let prompt_ptr = prompt_cstr
725 .as_ref()
726 .map(|c| c.as_ptr())
727 .unwrap_or(std::ptr::null());
728 let sampling_ptr = sampling_cstr
729 .as_ref()
730 .map(|c| c.as_ptr())
731 .unwrap_or(std::ptr::null());
732
733 let result =
734 onde_engine_load_default_model(engine as *mut c_void, prompt_ptr, sampling_ptr);
735 cstring_ptr_to_jstring(&mut env, result)
736 }
737
738 #[no_mangle]
739 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEngineLoadModel(
740 mut env: JNIEnv,
741 _class: JClass,
742 engine: jlong,
743 config_json: JString,
744 system_prompt: JString,
745 sampling_json: JString,
746 ) -> jstring {
747 let config_cstr = jstring_to_cstring(&mut env, &config_json);
748 let prompt_cstr = jstring_to_cstring(&mut env, &system_prompt);
749 let sampling_cstr = jstring_to_cstring(&mut env, &sampling_json);
750
751 let config_ptr = config_cstr
752 .as_ref()
753 .map(|c| c.as_ptr())
754 .unwrap_or(std::ptr::null());
755 let prompt_ptr = prompt_cstr
756 .as_ref()
757 .map(|c| c.as_ptr())
758 .unwrap_or(std::ptr::null());
759 let sampling_ptr = sampling_cstr
760 .as_ref()
761 .map(|c| c.as_ptr())
762 .unwrap_or(std::ptr::null());
763
764 let result =
765 onde_engine_load_model(engine as *mut c_void, config_ptr, prompt_ptr, sampling_ptr);
766 cstring_ptr_to_jstring(&mut env, result)
767 }
768
769 #[no_mangle]
770 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEngineLoadUqffModel(
771 mut env: JNIEnv,
772 _class: JClass,
773 engine: jlong,
774 config_json: JString,
775 system_prompt: JString,
776 sampling_json: JString,
777 ) -> jstring {
778 let config_cstr = jstring_to_cstring(&mut env, &config_json);
779 let prompt_cstr = jstring_to_cstring(&mut env, &system_prompt);
780 let sampling_cstr = jstring_to_cstring(&mut env, &sampling_json);
781
782 let config_ptr = config_cstr
783 .as_ref()
784 .map(|c| c.as_ptr())
785 .unwrap_or(std::ptr::null());
786 let prompt_ptr = prompt_cstr
787 .as_ref()
788 .map(|c| c.as_ptr())
789 .unwrap_or(std::ptr::null());
790 let sampling_ptr = sampling_cstr
791 .as_ref()
792 .map(|c| c.as_ptr())
793 .unwrap_or(std::ptr::null());
794
795 let result = onde_engine_load_uqff_model(
796 engine as *mut c_void,
797 config_ptr,
798 prompt_ptr,
799 sampling_ptr,
800 );
801 cstring_ptr_to_jstring(&mut env, result)
802 }
803
804 #[no_mangle]
805 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEngineUnloadModel(
806 mut env: JNIEnv,
807 _class: JClass,
808 engine: jlong,
809 ) -> jstring {
810 let result = onde_engine_unload_model(engine as *mut c_void);
811 cstring_ptr_to_jstring(&mut env, result)
812 }
813
814 #[no_mangle]
815 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEngineIsLoaded(
816 _env: JNIEnv,
817 _class: JClass,
818 engine: jlong,
819 ) -> jboolean {
820 if onde_engine_is_loaded(engine as *mut c_void) {
821 JNI_TRUE
822 } else {
823 JNI_FALSE
824 }
825 }
826
827 // ── Engine info ──────────────────────────────────────────────────────
828
829 #[no_mangle]
830 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEngineInfo(
831 mut env: JNIEnv,
832 _class: JClass,
833 engine: jlong,
834 ) -> jstring {
835 let result = onde_engine_info(engine as *mut c_void);
836 cstring_ptr_to_jstring(&mut env, result)
837 }
838
839 // ── System prompt ────────────────────────────────────────────────────
840
841 #[no_mangle]
842 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEngineSetSystemPrompt(
843 mut env: JNIEnv,
844 _class: JClass,
845 engine: jlong,
846 prompt: JString,
847 ) {
848 let prompt_cstr = jstring_to_cstring(&mut env, &prompt);
849 let prompt_ptr = prompt_cstr
850 .as_ref()
851 .map(|c| c.as_ptr())
852 .unwrap_or(std::ptr::null());
853 onde_engine_set_system_prompt(engine as *mut c_void, prompt_ptr);
854 }
855
856 #[no_mangle]
857 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEngineClearSystemPrompt(
858 _env: JNIEnv,
859 _class: JClass,
860 engine: jlong,
861 ) {
862 onde_engine_clear_system_prompt(engine as *mut c_void);
863 }
864
865 // ── Sampling ─────────────────────────────────────────────────────────
866
867 #[no_mangle]
868 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEngineSetSampling(
869 mut env: JNIEnv,
870 _class: JClass,
871 engine: jlong,
872 sampling_json: JString,
873 ) {
874 let sampling_cstr = jstring_to_cstring(&mut env, &sampling_json);
875 let sampling_ptr = sampling_cstr
876 .as_ref()
877 .map(|c| c.as_ptr())
878 .unwrap_or(std::ptr::null());
879 onde_engine_set_sampling(engine as *mut c_void, sampling_ptr);
880 }
881
882 // ── History ──────────────────────────────────────────────────────────
883
884 #[no_mangle]
885 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEngineHistory(
886 mut env: JNIEnv,
887 _class: JClass,
888 engine: jlong,
889 ) -> jstring {
890 let result = onde_engine_history(engine as *mut c_void);
891 cstring_ptr_to_jstring(&mut env, result)
892 }
893
894 #[no_mangle]
895 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEngineClearHistory(
896 _env: JNIEnv,
897 _class: JClass,
898 engine: jlong,
899 ) -> jlong {
900 onde_engine_clear_history(engine as *mut c_void) as jlong
901 }
902
903 #[no_mangle]
904 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEnginePushHistory(
905 mut env: JNIEnv,
906 _class: JClass,
907 engine: jlong,
908 message_json: JString,
909 ) {
910 let msg_cstr = jstring_to_cstring(&mut env, &message_json);
911 let msg_ptr = msg_cstr
912 .as_ref()
913 .map(|c| c.as_ptr())
914 .unwrap_or(std::ptr::null());
915 onde_engine_push_history(engine as *mut c_void, msg_ptr);
916 }
917
918 // ── Inference ────────────────────────────────────────────────────────
919
920 #[no_mangle]
921 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEngineSendMessage(
922 mut env: JNIEnv,
923 _class: JClass,
924 engine: jlong,
925 message: JString,
926 ) -> jstring {
927 let msg_cstr = jstring_to_cstring(&mut env, &message);
928 let msg_ptr = msg_cstr
929 .as_ref()
930 .map(|c| c.as_ptr())
931 .unwrap_or(std::ptr::null());
932 let result = onde_engine_send_message(engine as *mut c_void, msg_ptr);
933 cstring_ptr_to_jstring(&mut env, result)
934 }
935
936 #[no_mangle]
937 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEngineGenerate(
938 mut env: JNIEnv,
939 _class: JClass,
940 engine: jlong,
941 messages_json: JString,
942 sampling_json: JString,
943 ) -> jstring {
944 let msgs_cstr = jstring_to_cstring(&mut env, &messages_json);
945 let sampling_cstr = jstring_to_cstring(&mut env, &sampling_json);
946
947 let msgs_ptr = msgs_cstr
948 .as_ref()
949 .map(|c| c.as_ptr())
950 .unwrap_or(std::ptr::null());
951 let sampling_ptr = sampling_cstr
952 .as_ref()
953 .map(|c| c.as_ptr())
954 .unwrap_or(std::ptr::null());
955
956 let result = onde_engine_generate(engine as *mut c_void, msgs_ptr, sampling_ptr);
957 cstring_ptr_to_jstring(&mut env, result)
958 }
959
960 // ── Model config presets ─────────────────────────────────────────────
961
962 #[no_mangle]
963 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeDefaultModelConfig(
964 mut env: JNIEnv,
965 _class: JClass,
966 ) -> jstring {
967 let result = onde_default_model_config();
968 cstring_ptr_to_jstring(&mut env, result)
969 }
970
971 #[no_mangle]
972 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeUqffModelConfig(
973 mut env: JNIEnv,
974 _class: JClass,
975 model_id: JString,
976 files_json: JString,
977 display_name: JString,
978 approx_memory: JString,
979 chat_template: JString,
980 ) -> jstring {
981 let model_id_cstr = jstring_to_cstring(&mut env, &model_id);
982 let files_cstr = jstring_to_cstring(&mut env, &files_json);
983 let display_name_cstr = jstring_to_cstring(&mut env, &display_name);
984 let approx_memory_cstr = jstring_to_cstring(&mut env, &approx_memory);
985 let chat_template_cstr = jstring_to_cstring(&mut env, &chat_template);
986
987 let result = onde_uqff_model_config(
988 model_id_cstr
989 .as_ref()
990 .map(|c| c.as_ptr())
991 .unwrap_or(std::ptr::null()),
992 files_cstr
993 .as_ref()
994 .map(|c| c.as_ptr())
995 .unwrap_or(std::ptr::null()),
996 display_name_cstr
997 .as_ref()
998 .map(|c| c.as_ptr())
999 .unwrap_or(std::ptr::null()),
1000 approx_memory_cstr
1001 .as_ref()
1002 .map(|c| c.as_ptr())
1003 .unwrap_or(std::ptr::null()),
1004 chat_template_cstr
1005 .as_ref()
1006 .map(|c| c.as_ptr())
1007 .unwrap_or(std::ptr::null()),
1008 );
1009 cstring_ptr_to_jstring(&mut env, result)
1010 }
1011
1012 #[no_mangle]
1013 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeQwen251_15bConfig(
1014 mut env: JNIEnv,
1015 _class: JClass,
1016 ) -> jstring {
1017 let result = onde_qwen25_1_5b_config();
1018 cstring_ptr_to_jstring(&mut env, result)
1019 }
1020
1021 #[no_mangle]
1022 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeQwen253bConfig(
1023 mut env: JNIEnv,
1024 _class: JClass,
1025 ) -> jstring {
1026 let result = onde_qwen25_3b_config();
1027 cstring_ptr_to_jstring(&mut env, result)
1028 }
1029
1030 #[no_mangle]
1031 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeQwen3_10_16bConfig(
1032 mut env: JNIEnv,
1033 _class: JClass,
1034 ) -> jstring {
1035 cstring_ptr_to_jstring(&mut env, onde_qwen3_0_6b_config())
1036 }
1037
1038 #[no_mangle]
1039 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeQwen3_11_17bConfig(
1040 mut env: JNIEnv,
1041 _class: JClass,
1042 ) -> jstring {
1043 cstring_ptr_to_jstring(&mut env, onde_qwen3_1_7b_config())
1044 }
1045
1046 #[no_mangle]
1047 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeQwen3_14bConfig(
1048 mut env: JNIEnv,
1049 _class: JClass,
1050 ) -> jstring {
1051 cstring_ptr_to_jstring(&mut env, onde_qwen3_4b_config())
1052 }
1053
1054 #[no_mangle]
1055 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeQwen3_18bConfig(
1056 mut env: JNIEnv,
1057 _class: JClass,
1058 ) -> jstring {
1059 cstring_ptr_to_jstring(&mut env, onde_qwen3_8b_config())
1060 }
1061
1062 #[no_mangle]
1063 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeQwen3_114bConfig(
1064 mut env: JNIEnv,
1065 _class: JClass,
1066 ) -> jstring {
1067 cstring_ptr_to_jstring(&mut env, onde_qwen3_14b_config())
1068 }
1069
1070 #[no_mangle]
1071 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeQwen3_132bConfig(
1072 mut env: JNIEnv,
1073 _class: JClass,
1074 ) -> jstring {
1075 cstring_ptr_to_jstring(&mut env, onde_qwen3_32b_config())
1076 }
1077
1078 #[no_mangle]
1079 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeQwen3_14bInstruct2507Config(
1080 mut env: JNIEnv,
1081 _class: JClass,
1082 ) -> jstring {
1083 cstring_ptr_to_jstring(&mut env, onde_qwen3_4b_instruct_2507_config())
1084 }
1085
1086 #[no_mangle]
1087 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeQwen3_14bThinking2507Config(
1088 mut env: JNIEnv,
1089 _class: JClass,
1090 ) -> jstring {
1091 cstring_ptr_to_jstring(&mut env, onde_qwen3_4b_thinking_2507_config())
1092 }
1093
1094 #[no_mangle]
1095 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeQwen3_130bA3bInstruct2507Config(
1096 mut env: JNIEnv,
1097 _class: JClass,
1098 ) -> jstring {
1099 cstring_ptr_to_jstring(&mut env, onde_qwen3_30b_a3b_instruct_2507_config())
1100 }
1101
1102 // ── Sampling presets ─────────────────────────────────────────────────
1103
1104 #[no_mangle]
1105 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeDefaultSamplingConfig(
1106 mut env: JNIEnv,
1107 _class: JClass,
1108 ) -> jstring {
1109 let result = onde_default_sampling_config();
1110 cstring_ptr_to_jstring(&mut env, result)
1111 }
1112
1113 #[no_mangle]
1114 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeDeterministicSamplingConfig(
1115 mut env: JNIEnv,
1116 _class: JClass,
1117 ) -> jstring {
1118 let result = onde_deterministic_sampling_config();
1119 cstring_ptr_to_jstring(&mut env, result)
1120 }
1121
1122 #[no_mangle]
1123 pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeMobileSamplingConfig(
1124 mut env: JNIEnv,
1125 _class: JClass,
1126 ) -> jstring {
1127 let result = onde_mobile_sampling_config();
1128 cstring_ptr_to_jstring(&mut env, result)
1129 }
1130 }