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
// ---------------------------------------------------------------------------
// Bridge object emitter
// ---------------------------------------------------------------------------
/// Emit `<PascalCrateName>Bridge.kt` — a Kotlin `object` containing:
/// - `init { System.loadLibrary("<crate>_jni") }`
/// - `external fun native<Method>(...)` for every visible API function
/// - `external fun native{Owner}{Adapter}{Start,Next,Free}` for every
/// streaming adapter with an `owner_type`.
pub fn emit_jni_bridge_object(api: &ApiSurface, config: &ResolvedCrateConfig) -> GeneratedFile {
let module_name = to_pascal_case(&config.name);
let bridge_name = format!("{module_name}Bridge");
// The exception class is emitted alongside the Bridge object and referenced in
// @Throws annotations so that callers can catch typed JNI errors.
let exception_class = format!("{bridge_name}Exception");
let lib_name = config.jni_lib_name();
let package = jni_kotlin_package(config);
let exclude_functions: std::collections::HashSet<&str> = config
.kotlin_android
.as_ref()
.map(|c| c.exclude_functions.iter().map(String::as_str).collect())
.unwrap_or_else(|| {
config
.kotlin
.as_ref()
.map(|k| k.exclude_functions.iter().map(String::as_str).collect())
.unwrap_or_default()
});
let visible_functions: Vec<_> = api
.functions
.iter()
.filter(|f| !exclude_functions.contains(f.name.as_str()))
.collect();
// Opaque type names: Named params of this shape are handles (Long), not JSON (String).
let opaque_type_names: std::collections::HashSet<&str> = api
.types
.iter()
.filter(|t| t.is_opaque && !t.is_trait)
.map(|t| t.name.as_str())
.collect();
let mut body = String::new();
// Suppress detekt TooManyFunctions: the bridge object has one external fun
// per API function; large APIs naturally exceed the default threshold of 11.
body.push_str(&template_env::render(
"jni_bridge_object_header.jinja",
minijinja::context! {
bridge_name => bridge_name,
lib_name => lib_name,
},
));
// Collect native function names from the API to detect duplicates later.
let mut emitted_native_names: std::collections::HashSet<String> = std::collections::HashSet::new();
// Track destructor names that have been emitted to avoid duplication.
let mut emitted_destructor_names: std::collections::HashSet<String> = std::collections::HashSet::new();
// Emit one `external fun` per visible API function.
// Every native method is annotated @Throws so typed catch blocks work in
// Kotlin/Java callers — without this the JNI RuntimeException is wrapped in
// UndeclaredThrowableException and silently bypasses catch(BridgeException).
for f in &visible_functions {
let native_name = format!("native{}", to_pascal_case(&f.name));
emitted_native_names.insert(native_name.clone());
let return_ty = jni_return_type_for_function(&f.return_type, &opaque_type_names);
let jni_params = jni_params_for_function(f, &opaque_type_names);
body.push('\n');
push_jni_external_fun(
&mut body,
&native_name,
&jni_params,
non_unit_return_type(&f.return_type, return_ty),
Some(&exception_class),
);
}
// Emit external funs for instance methods on opaque client types.
let methods_emitted_before = body.matches("// JNI external funs for client instance methods").count();
emit_method_jni_external_funs(
&mut body,
api,
&exclude_functions,
&exception_class,
&mut emitted_destructor_names,
);
let methods_emitted_after = body.matches("// JNI external funs for client instance methods").count();
// Fallback: if emit_method_jni_external_funs didn't emit the comment (no client types found),
// manually emit declarations for any opaque types with methods that the client generator found.
if methods_emitted_before == methods_emitted_after {
// Try to find opaque client types by looking for those with methods
let opaque_with_methods: Vec<_> = api
.types
.iter()
.filter(|t| {
t.is_opaque
&& !t.is_trait
&& !t.methods.is_empty()
&& !exclude_functions
.iter()
.all(|&excluded| t.methods.iter().all(|m| excluded == m.name.as_str()))
})
.collect();
if !opaque_with_methods.is_empty() {
body.push_str("\n // JNI external funs for client instance methods (fallback).\n");
for ty in &opaque_with_methods {
let owner_pascal = to_pascal_case(&ty.name);
for method in &ty.methods {
if exclude_functions.contains(method.name.as_str()) {
continue;
}
let native_name = format!("native{owner_pascal}{}", to_pascal_case(&method.name));
let return_ty = jni_return_type(&method.return_type);
let params = if method.params.is_empty() {
"handle: Long".to_string()
} else if method.params.len() == 1 && is_binary_param_type(&method.params[0].ty) {
format!("handle: Long, {}: ByteArray", to_lower_camel(&method.params[0].name))
} else {
"handle: Long, requestJson: String".to_string()
};
push_jni_external_fun(
&mut body,
&native_name,
¶ms,
non_unit_return_type(&method.return_type, return_ty),
Some(&exception_class),
);
}
}
}
}
// Emit streaming external funs.
emit_streaming_jni_external_funs(&mut body, config, &exception_class);
// Emit nativeNew<TypeName> external funs for client_constructors entries.
emit_constructor_jni_external_funs(&mut body, api, config, &exception_class);
// Emit nativeRegister<Trait> / nativeUnregister<Trait> / nativeClear<Trait>s
// external funs for every [[crates.trait_bridges]] entry whose configuration
// does not exclude `kotlin_android`. Skip duplicates already emitted from the API.
emit_trait_bridge_jni_external_funs(&mut body, config, &exception_class, &package, &emitted_native_names);
// Emit nativeFreeXxx destructors for opaque types returned by top-level functions
// that do NOT have instance methods. Client type destructors are already emitted
// by emit_method_jni_external_funs at line 326 for ALL types with methods,
// including those that may also be returned by top-level functions.
let client_type_names: std::collections::HashSet<&str> = api
.types
.iter()
.filter(|t| t.is_opaque && !t.is_trait && t.methods.iter().any(|m| !m.sanitized && !m.is_static))
.map(|t| t.name.as_str())
.collect();
// Emit a `nativeFree<TypeName>` destructor for every opaque non-trait type
// that is NOT a client. This mirrors the kotlin_android wrapper emitter
// (`gen_bindings::emit_module_kt`), which now materialises an
// AutoCloseable wrapper class for every opaque non-client type — its
// `close()` body calls `Bridge.nativeFree{TypeName}(handle)`, so the JNI
// bridge MUST declare a matching external fun or Kotlin compilation fails
// with `Unresolved reference 'nativeFree<TypeName>'`.
//
// The previous filter only considered return types of top-level
// functions, which missed opaque types whose only public entrypoint is a
// static factory method (kept as `@staticmethod` on the class rather than
// lifted to a free function in alef's IR — e.g. `TokenCounter::new()`).
// The FFI layer still emits the `{prefix}_{type_snake}_free` C symbol
// unconditionally for every opaque type, so the JNI side has a real
// function to bind against.
let handle_only_opaque_returns: std::collections::BTreeSet<&str> = api
.types
.iter()
.filter(|t| t.is_opaque && !t.is_trait && !client_type_names.contains(t.name.as_str()))
.map(|t| t.name.as_str())
.collect();
// Emit destructors ONLY for handle-only types (top-level returns, not client types).
// Skip any that were already emitted to avoid duplicates.
if !handle_only_opaque_returns.is_empty() {
body.push_str("\n // Destructor external funs for handle-only opaque types.\n");
for type_name in &handle_only_opaque_returns {
let free_name = format!("nativeFree{}", to_pascal_case(type_name));
if !emitted_destructor_names.contains(&free_name) {
push_jni_external_fun(&mut body, &free_name, "handle: Long", None, None);
}
}
}
body.push_str("}\n");
let content = template_env::render(
"jni_bridge_file.jinja",
minijinja::context! {
package => package,
body => body,
},
);
let path = jni_output_path(config, &format!("{bridge_name}.kt"));
GeneratedFile {
path,
content,
generated_header: false,
}
}