Skip to main content

uniffi_bindgen_java/
lib.rs

1use anyhow::{Context, Result};
2use camino::{Utf8Path, Utf8PathBuf};
3use clap::{Parser, Subcommand};
4use std::collections::HashMap;
5use std::fs;
6use uniffi_bindgen::{
7    BindgenLoader, BindgenPaths, Component, ComponentInterface, interface::rename,
8};
9
10mod gen_java;
11use gen_java::Config;
12
13/// Options for generating Java bindings
14pub struct GenerateOptions {
15    /// Path to the source file (UDL or library)
16    pub source: Utf8PathBuf,
17    /// Directory to write generated files
18    pub out_dir: Utf8PathBuf,
19    /// Whether to format generated code (currently not implemented)
20    pub format: bool,
21    /// Optional crate filter - only generate bindings for this crate
22    pub crate_filter: Option<String>,
23}
24
25pub fn generate(loader: &BindgenLoader, options: &GenerateOptions) -> Result<()> {
26    let metadata = loader.load_metadata(&options.source)?;
27    let cis = loader.load_cis(metadata)?;
28    let cdylib = loader.library_name(&options.source).map(|l| l.to_string());
29    let mut components =
30        loader.load_components(cis, |ci, toml| parse_config(ci, toml, cdylib.clone()))?;
31
32    // Apply renames and update external package mappings (must happen before derive_ffi_funcs)
33    apply_renames_and_external_packages(&mut components);
34
35    // Derive FFI functions for each component (after renames)
36    for c in components.iter_mut() {
37        c.ci.derive_ffi_funcs()?;
38    }
39
40    // Generate and write bindings for each component
41    let filename_capture = regex::Regex::new(
42        r"(?m)^(?:public\s)?(?:final\s)?(?:sealed\s)?(?:abstract\s)?(?:static\s)?(?:class|interface|enum|record)\s(\w+)",
43    )
44    .unwrap();
45
46    for Component { ci, config, .. } in components {
47        if let Some(crate_filter) = &options.crate_filter
48            && ci.crate_name() != crate_filter
49        {
50            continue;
51        }
52
53        let bindings_str = gen_java::generate_bindings(&config, &ci)?;
54        let java_package_out_dir = options.out_dir.join(
55            config
56                .package_name()
57                .split('.')
58                .collect::<Vec<_>>()
59                .join("/"),
60        );
61        fs::create_dir_all(&java_package_out_dir)?;
62
63        let package_line = format!("package {};", config.package_name());
64        let split_classes = bindings_str.split(&package_line);
65        let writable = split_classes
66            .map(|file| (filename_capture.captures(file), file))
67            .filter(|(x, _)| x.is_some())
68            .map(|(captures, file)| (captures.unwrap().get(1).unwrap().as_str(), file))
69            .collect::<Vec<_>>();
70
71        for (filename, file) in writable {
72            let java_file_location = java_package_out_dir.join(format!("{}.java", filename));
73            fs::write(&java_file_location, format!("{}\n{}", package_line, file))?;
74        }
75
76        if config.nullness_annotations() {
77            let package_info = format!("@org.jspecify.annotations.NullMarked\n{}", package_line);
78            fs::write(java_package_out_dir.join("package-info.java"), package_info)?;
79        }
80
81        if options.format {
82            // TODO: if there's a CLI formatter that makes sense to use here, use it, PRs welcome
83            // seems like palantir-java-format is popular, but it's only exposed through plugins
84            // google-java-format is legacy popular and does have an executable all-deps JAR, but
85            // must be called with the full jar path including version numbers
86            // prettier sorta works but requires npm and packages be around for a java generator
87        }
88    }
89    Ok(())
90}
91
92/// Parse Java configuration from TOML
93fn parse_config(
94    ci: &ComponentInterface,
95    root_toml: toml::Value,
96    cdylib: Option<String>,
97) -> Result<Config> {
98    let mut config: Config = match root_toml.get("bindings").and_then(|b| b.get("java")) {
99        Some(v) => v.clone().try_into()?,
100        None => Default::default(),
101    };
102    config
103        .package_name
104        .get_or_insert_with(|| format!("uniffi.{}", ci.namespace()));
105    config.cdylib_name.get_or_insert_with(|| {
106        cdylib
107            .clone()
108            .unwrap_or_else(|| format!("uniffi_{}", ci.namespace()))
109    });
110    Ok(config)
111}
112
113/// Apply rename configurations and update external package mappings across all components.
114/// This must be called before derive_ffi_funcs() since renames affect FFI function names.
115fn apply_renames_and_external_packages(components: &mut Vec<Component<Config>>) {
116    // Collect all rename configurations from all components, keyed by module_path (crate name)
117    let mut module_renames = HashMap::new();
118    for c in components.iter() {
119        if !c.config.rename.is_empty() {
120            let module_path = c.ci.crate_name().to_string();
121            module_renames.insert(module_path, c.config.rename.clone());
122        }
123    }
124
125    // Apply rename configurations to all components
126    if !module_renames.is_empty() {
127        for c in &mut *components {
128            rename(&mut c.ci, &module_renames);
129        }
130    }
131
132    // Update external package mappings
133    let packages = HashMap::<String, String>::from_iter(
134        components
135            .iter()
136            .map(|c| (c.ci.crate_name().to_string(), c.config.package_name())),
137    );
138    for c in components {
139        for (ext_crate, ext_package) in &packages {
140            if ext_crate != c.ci.crate_name() && !c.config.external_packages.contains_key(ext_crate)
141            {
142                c.config
143                    .external_packages
144                    .insert(ext_crate.to_string(), ext_package.clone());
145            }
146        }
147    }
148}
149
150/// Create BindgenPaths with cargo metadata layer and optional config override
151fn create_bindgen_paths(
152    config_override: Option<&Utf8Path>,
153    metadata_no_deps: bool,
154) -> Result<BindgenPaths> {
155    let mut paths = BindgenPaths::default();
156
157    // Add config override layer first (takes precedence)
158    if let Some(config_path) = config_override {
159        paths.add_config_override_layer(config_path.to_path_buf());
160    }
161
162    // Add cargo metadata layer for finding crate configs
163    paths
164        .add_cargo_metadata_layer(metadata_no_deps)
165        .context("Failed to load cargo metadata")?;
166
167    Ok(paths)
168}
169
170#[derive(Parser)]
171#[clap(name = "uniffi-bindgen-java")]
172#[clap(version = clap::crate_version!())]
173#[clap(propagate_version = true, disable_help_subcommand = true)]
174/// Java scaffolding and bindings generator for Rust
175struct Cli {
176    #[clap(subcommand)]
177    command: Commands,
178}
179
180#[derive(Subcommand)]
181enum Commands {
182    /// Generate Java bindings
183    Generate {
184        /// Directory in which to write generated files. Default is same folder as .udl file.
185        #[clap(long, short)]
186        out_dir: Option<Utf8PathBuf>,
187
188        /// Do not try to format the generated bindings.
189        #[clap(long, short)]
190        no_format: bool,
191
192        /// Path to optional uniffi config file. This config is merged with the `uniffi.toml` config present in each crate, with its values taking precedence.
193        #[clap(long, short)]
194        config: Option<Utf8PathBuf>,
195
196        /// When a library is passed as SOURCE, only generate bindings for this crate.
197        /// When a UDL file is passed, use this as the crate name instead of attempting to
198        /// locate and parse Cargo.toml.
199        #[clap(long = "crate")]
200        crate_name: Option<String>,
201
202        /// Path to the UDL file or compiled library (.so, .dll, .dylib, or .a)
203        source: Utf8PathBuf,
204
205        /// Whether we should exclude dependencies when running "cargo metadata".
206        /// This will mean external types may not be resolved if they are implemented in crates
207        /// outside of this workspace.
208        /// This can be used in environments when all types are in the namespace and fetching
209        /// all sub-dependencies causes obscure platform specific problems.
210        #[clap(long)]
211        metadata_no_deps: bool,
212    },
213    /// Generate Rust scaffolding code
214    Scaffolding {
215        /// Directory in which to write generated files. Default is same folder as .udl file.
216        #[clap(long, short)]
217        out_dir: Option<Utf8PathBuf>,
218
219        /// Do not try to format the generated bindings.
220        #[clap(long, short)]
221        no_format: bool,
222
223        /// Path to the UDL file.
224        udl_file: Utf8PathBuf,
225    },
226    /// Print a debug representation of the interface from a dynamic library
227    PrintRepr {
228        /// Path to the library file (.so, .dll, .dylib, or .a)
229        path: Utf8PathBuf,
230    },
231}
232
233pub fn run_main() -> Result<()> {
234    let cli = Cli::parse();
235    match cli.command {
236        Commands::Generate {
237            out_dir,
238            no_format,
239            config,
240            crate_name,
241            source,
242            metadata_no_deps,
243        } => {
244            let out_dir = out_dir.unwrap_or_else(|| {
245                source
246                    .parent()
247                    .map(|p| p.to_path_buf())
248                    .unwrap_or_else(|| Utf8PathBuf::from("."))
249            });
250
251            // Create BindgenPaths with cargo metadata and optional config override
252            let paths = create_bindgen_paths(config.as_deref(), metadata_no_deps)?;
253            let loader = BindgenLoader::new(paths);
254
255            fs::create_dir_all(&out_dir)?;
256
257            generate(
258                &loader,
259                &GenerateOptions {
260                    source,
261                    out_dir,
262                    format: !no_format,
263                    crate_filter: crate_name,
264                },
265            )?;
266        }
267        Commands::Scaffolding {
268            out_dir,
269            no_format,
270            udl_file,
271        } => {
272            uniffi_bindgen::generate_component_scaffolding(
273                &udl_file,
274                out_dir.as_deref(),
275                !no_format,
276            )?;
277        }
278        Commands::PrintRepr { path } => {
279            uniffi_bindgen::print_repr(&path)?;
280        }
281    };
282    Ok(())
283}