1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
/// Emit a shim for an instance method on an opaque client type.
///
/// `receiver_is_mut` controls whether the handle is cast to `*mut T` (`&mut self`)
/// or `*const T` (`&self`). `opaque_type_names` is used to identify handle-typed
/// params so they can be received as `jlong` rather than a JSON string.
#[allow(clippy::too_many_arguments)]
fn emit_method_shim(
out: &mut String,
symbol: &str,
type_name: &str,
method_name: &str,
params: &[ParamDef],
return_type: &TypeRef,
is_async: bool,
has_error: bool,
receiver_is_mut: bool,
receiver_owned: bool,
opaque_type_names: &std::collections::HashSet<&str>,
) {
let rust_method = method_name.replace('-', "_");
let has_params = !params.is_empty();
// Direct opaque return: `-> NamedType` where the type is opaque.
let is_opaque_return = matches!(return_type, TypeRef::Named(n) if opaque_type_names.contains(n.as_str()));
// Optional opaque return: `-> Option<NamedType>` where the inner type is opaque.
let is_optional_opaque_return = matches!(
return_type,
TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Named(n) if opaque_type_names.contains(n.as_str()))
);
let ret_decl = if is_opaque_return || is_optional_opaque_return {
" -> jlong".to_string()
} else {
method_return_type_decl(return_type)
};
let ret_null = if is_opaque_return || is_optional_opaque_return {
"0"
} else {
method_return_null(return_type)
};
// For single-param methods with Vec<u8>/Bytes params: use jbyteArray as the
// JNI parameter type (param name matches the rust param name, not request_json).
// All other single-param and all multi-param methods use request_json: JString.
let request_param = if !has_params {
String::new()
} else if params.len() == 1 {
let p = ¶ms[0];
let rust_name = p.name.replace('-', "_");
let base_ty = match &p.ty {
TypeRef::Optional(inner) => inner.as_ref(),
other => other,
};
match base_ty {
TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::U8)) => {
render_param_decl(&rust_name, "jbyteArray")
}
TypeRef::Bytes => render_param_decl(&rust_name, "jbyteArray"),
_ => " request_json: JString,\n".to_string(),
}
} else {
" request_json: JString,\n".to_string()
};
// See emit_function_shim for why we use AttachGuard::from_unowned instead
// of EnvUnowned::with_env.
out.push_str(&template_env::render(
"method_shim_open.rs.jinja",
context! {
symbol => symbol,
request_param => request_param,
ret_decl => ret_decl,
},
));
// Dereference handle.
out.push_str(&template_env::render(
"method_client_handle.rs.jinja",
context! {
receiver_owned => receiver_owned,
receiver_is_mut => receiver_is_mut,
type_name => type_name,
},
));
// Unmarshal params and build call_args with is_ref/optional adjustments.
let call_args: String = if !has_params {
String::new()
} else if params.len() == 1 {
let p = ¶ms[0];
let rust_name = p.name.replace('-', "_");
// Unwrap Optional wrapper for the JNI unmarshal type.
let base_ty = match &p.ty {
TypeRef::Optional(inner) => inner.as_ref(),
other => other,
};
// Branches that understand the target's optional sentinel produce an
// `Option<T>` binding directly. Other special cases bind the unwrapped
// `T` and need `Some(name)` wrapping at the call site.
let unmarshal_produces_option = p.optional
&& (matches!(base_ty, TypeRef::Bytes)
|| matches!(base_ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::U8)))
|| !matches!(base_ty, TypeRef::Vec(_) | TypeRef::Path | TypeRef::String));
emit_single_param_unmarshal(out, &rust_name, base_ty, ret_null, unmarshal_produces_option, p.map_is_btree);
// `&Vec<String>` coerces to `&[String]` so plain `&<name>` covers every
// Vec<String> method call we currently emit. The previous special-case
// that converted to `Vec<&str>` produced `&[&str]` which is incompatible
// with core methods that take `&[String]` (e.g. `LlmBackend::detect_with_custom`).
// Exception: core methods declared as `&[&str]` (IR `vec_inner_is_ref = true`
// on a `Vec<String>` slot) need a materialised `Vec<&str>` borrow.
if unmarshal_produces_option {
// Binding is already `Option<T>`. For an optional byte slice the core
// wants `Option<&[u8]>`; `Option<Vec<u8>>` does not coerce, so deref it.
if p.is_ref && is_byte_slice(base_ty) {
format!("{rust_name}.as_deref()")
} else {
rust_name
}
} else if p.optional {
format!("Some({rust_name})")
} else if needs_vec_string_refs(p, base_ty) {
out.push_str(&render_vec_string_refs_binding(&rust_name));
vec_string_refs_arg(&rust_name)
} else if p.is_ref {
format!("&{rust_name}")
} else {
rust_name
}
} else {
// Multi-param: decode JSON map.
out.push_str(&template_env::render(
"request_map_unmarshal.rs.jinja",
context! {
ret_null => ret_null,
},
));
let mut args = Vec::new();
for p in params {
let rust_name = p.name.replace('-', "_");
// Unwrap Optional for the deserialization type.
let base_ty = match &p.ty {
TypeRef::Optional(inner) => inner.as_ref(),
other => other,
};
// Byte-slice and Path params are not JSON-native at the call site, so the
// generic `type_ref_to_core_path_*` (which maps both to its `serde_json::Value`
// catch-all) produces a binding that fails E0308: `&Value` is neither `&[u8]`
// nor `&Path`. Bind the concrete owned type that derefs to the borrowed core
// type instead (`Vec<u8>` → `&[u8]`, `PathBuf` → `&Path`). This mirrors the
// single-param path's special `Bytes`/`Path` arms in `emit_single_param_unmarshal`.
let is_path = matches!(base_ty, TypeRef::Path);
let type_path = if is_byte_slice(base_ty) {
"Vec<u8>".to_string()
} else if is_path {
// The wire value is a JSON-encoded path string; deserialize as `String`
// and convert below so the call site sees a real `PathBuf`.
"String".to_string()
} else {
type_ref_to_core_path_with_btree(base_ty, "core_crate", p.map_is_btree)
};
out.push_str(&template_env::render(
"request_map_param_unmarshal.rs.jinja",
context! {
name => rust_name,
type_path => type_path,
ret_null => ret_null,
},
));
if is_path {
// Re-bind the `String` extracted from the request map as a `PathBuf`.
// (The `path_unmarshal` template targets the single-param path, where the
// raw string lives in `req_str`; here the value is already bound to `name`.)
out.push_str(&format!(" let {rust_name} = std::path::PathBuf::from({rust_name});\n"));
}
// `&Vec<String>` coerces to `&[String]`; the previous Vec<&str>
// special-case produced `&[&str]` incompatible with `&[String]` core
// method signatures. See the matching branch above.
// Exception: when the core method declares `&[&str]` (IR
// `vec_inner_is_ref = true`), materialise a `Vec<&str>` and borrow.
let call_arg = if p.optional {
// `request_map_param_unmarshal` binds the unwrapped `T`; an optional
// byte slice (`Option<&[u8]>`) needs a borrow inside the `Some`.
if p.is_ref && is_byte_slice(base_ty) {
format!("Some(&{rust_name})")
} else {
format!("Some({rust_name})")
}
} else if needs_vec_string_refs(p, base_ty) {
out.push_str(&render_vec_string_refs_binding(&rust_name));
vec_string_refs_arg(&rust_name)
} else if p.is_ref {
format!("&{rust_name}")
} else {
rust_name
};
args.push(call_arg);
}
args.join(", ")
};
// Build the call.
let call_expr = if call_args.is_empty() {
format!("client.{rust_method}()")
} else {
format!("client.{rust_method}({call_args})")
};
if has_error {
let mut ok_body = String::new();
if is_opaque_return {
ok_body.push_str(" Box::into_raw(Box::new(v)) as jlong\n");
} else if is_optional_opaque_return {
ok_body.push_str(" match v {\n");
ok_body.push_str(" None => 0i64,\n");
ok_body.push_str(" Some(inner) => Box::into_raw(Box::new(inner)) as jlong,\n");
ok_body.push_str(" }\n");
} else {
emit_return_marshal(&mut ok_body, return_type, ret_null);
}
render_call_result_body(out, &call_expr, is_async, true, ret_null, &ok_body, "");
} else {
let mut value_body = String::new();
if is_opaque_return {
value_body.push_str(" Box::into_raw(Box::new(v)) as jlong\n");
} else if is_optional_opaque_return {
value_body.push_str(" match v {\n");
value_body.push_str(" None => 0i64,\n");
value_body.push_str(" Some(inner) => Box::into_raw(Box::new(inner)) as jlong,\n");
value_body.push_str(" }\n");
} else {
emit_return_marshal_with_indent(&mut value_body, return_type, " ", ret_null);
}
render_call_result_body(out, &call_expr, is_async, false, ret_null, "", &value_body);
}
}
fn render_call_result_body(
out: &mut String,
call_expr: &str,
is_async: bool,
has_error: bool,
ret_null: &str,
ok_body: &str,
value_body: &str,
) {
let async_call_expr = format!("runtime().block_on({call_expr})");
out.push_str(&template_env::render(
"call_result_body.rs.jinja",
context! {
call_expr => call_expr,
async_call_expr => async_call_expr,
is_async => is_async,
has_error => has_error,
ret_null => ret_null,
ok_body => ok_body,
value_body => value_body,
},
));
}