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 options.format {
77            // TODO: if there's a CLI formatter that makes sense to use here, use it, PRs welcome
78            // seems like palantir-java-format is popular, but it's only exposed through plugins
79            // google-java-format is legacy popular and does have an executable all-deps JAR, but
80            // must be called with the full jar path including version numbers
81            // prettier sorta works but requires npm and packages be around for a java generator
82        }
83    }
84    Ok(())
85}
86
87/// Parse Java configuration from TOML
88fn parse_config(
89    ci: &ComponentInterface,
90    root_toml: toml::Value,
91    cdylib: Option<String>,
92) -> Result<Config> {
93    let mut config: Config = match root_toml.get("bindings").and_then(|b| b.get("java")) {
94        Some(v) => v.clone().try_into()?,
95        None => Default::default(),
96    };
97    config
98        .package_name
99        .get_or_insert_with(|| format!("uniffi.{}", ci.namespace()));
100    config.cdylib_name.get_or_insert_with(|| {
101        cdylib
102            .clone()
103            .unwrap_or_else(|| format!("uniffi_{}", ci.namespace()))
104    });
105    Ok(config)
106}
107
108/// Apply rename configurations and update external package mappings across all components.
109/// This must be called before derive_ffi_funcs() since renames affect FFI function names.
110fn apply_renames_and_external_packages(components: &mut Vec<Component<Config>>) {
111    // Collect all rename configurations from all components, keyed by module_path (crate name)
112    let mut module_renames = HashMap::new();
113    for c in components.iter() {
114        if !c.config.rename.is_empty() {
115            let module_path = c.ci.crate_name().to_string();
116            module_renames.insert(module_path, c.config.rename.clone());
117        }
118    }
119
120    // Apply rename configurations to all components
121    if !module_renames.is_empty() {
122        for c in &mut *components {
123            rename(&mut c.ci, &module_renames);
124        }
125    }
126
127    // Update external package mappings
128    let packages = HashMap::<String, String>::from_iter(
129        components
130            .iter()
131            .map(|c| (c.ci.crate_name().to_string(), c.config.package_name())),
132    );
133    for c in components {
134        for (ext_crate, ext_package) in &packages {
135            if ext_crate != c.ci.crate_name() && !c.config.external_packages.contains_key(ext_crate)
136            {
137                c.config
138                    .external_packages
139                    .insert(ext_crate.to_string(), ext_package.clone());
140            }
141        }
142    }
143}
144
145/// Create BindgenPaths with cargo metadata layer and optional config override
146fn create_bindgen_paths(
147    config_override: Option<&Utf8Path>,
148    metadata_no_deps: bool,
149) -> Result<BindgenPaths> {
150    let mut paths = BindgenPaths::default();
151
152    // Add config override layer first (takes precedence)
153    if let Some(config_path) = config_override {
154        paths.add_config_override_layer(config_path.to_path_buf());
155    }
156
157    // Add cargo metadata layer for finding crate configs
158    paths
159        .add_cargo_metadata_layer(metadata_no_deps)
160        .context("Failed to load cargo metadata")?;
161
162    Ok(paths)
163}
164
165#[derive(Parser)]
166#[clap(name = "uniffi-bindgen-java")]
167#[clap(version = clap::crate_version!())]
168#[clap(propagate_version = true, disable_help_subcommand = true)]
169/// Java scaffolding and bindings generator for Rust
170struct Cli {
171    #[clap(subcommand)]
172    command: Commands,
173}
174
175#[derive(Subcommand)]
176enum Commands {
177    /// Generate Java bindings
178    Generate {
179        /// Directory in which to write generated files. Default is same folder as .udl file.
180        #[clap(long, short)]
181        out_dir: Option<Utf8PathBuf>,
182
183        /// Do not try to format the generated bindings.
184        #[clap(long, short)]
185        no_format: bool,
186
187        /// Path to optional uniffi config file. This config is merged with the `uniffi.toml` config present in each crate, with its values taking precedence.
188        #[clap(long, short)]
189        config: Option<Utf8PathBuf>,
190
191        /// When a library is passed as SOURCE, only generate bindings for this crate.
192        /// When a UDL file is passed, use this as the crate name instead of attempting to
193        /// locate and parse Cargo.toml.
194        #[clap(long = "crate")]
195        crate_name: Option<String>,
196
197        /// Path to the UDL file or compiled library (.so, .dll, .dylib, or .a)
198        source: Utf8PathBuf,
199
200        /// Whether we should exclude dependencies when running "cargo metadata".
201        /// This will mean external types may not be resolved if they are implemented in crates
202        /// outside of this workspace.
203        /// This can be used in environments when all types are in the namespace and fetching
204        /// all sub-dependencies causes obscure platform specific problems.
205        #[clap(long)]
206        metadata_no_deps: bool,
207    },
208    /// Generate Rust scaffolding code
209    Scaffolding {
210        /// Directory in which to write generated files. Default is same folder as .udl file.
211        #[clap(long, short)]
212        out_dir: Option<Utf8PathBuf>,
213
214        /// Do not try to format the generated bindings.
215        #[clap(long, short)]
216        no_format: bool,
217
218        /// Path to the UDL file.
219        udl_file: Utf8PathBuf,
220    },
221    /// Print a debug representation of the interface from a dynamic library
222    PrintRepr {
223        /// Path to the library file (.so, .dll, .dylib, or .a)
224        path: Utf8PathBuf,
225    },
226}
227
228pub fn run_main() -> Result<()> {
229    let cli = Cli::parse();
230    match cli.command {
231        Commands::Generate {
232            out_dir,
233            no_format,
234            config,
235            crate_name,
236            source,
237            metadata_no_deps,
238        } => {
239            let out_dir = out_dir.unwrap_or_else(|| {
240                source
241                    .parent()
242                    .map(|p| p.to_path_buf())
243                    .unwrap_or_else(|| Utf8PathBuf::from("."))
244            });
245
246            // Create BindgenPaths with cargo metadata and optional config override
247            let paths = create_bindgen_paths(config.as_deref(), metadata_no_deps)?;
248            let loader = BindgenLoader::new(paths);
249
250            fs::create_dir_all(&out_dir)?;
251
252            generate(
253                &loader,
254                &GenerateOptions {
255                    source,
256                    out_dir,
257                    format: !no_format,
258                    crate_filter: crate_name,
259                },
260            )?;
261        }
262        Commands::Scaffolding {
263            out_dir,
264            no_format,
265            udl_file,
266        } => {
267            uniffi_bindgen::generate_component_scaffolding(
268                &udl_file,
269                out_dir.as_deref(),
270                !no_format,
271            )?;
272        }
273        Commands::PrintRepr { path } => {
274            uniffi_bindgen::print_repr(&path)?;
275        }
276    };
277    Ok(())
278}