diff --git a/CHANGELOG.md b/CHANGELOG.md index e69e344..d976b5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - `--lib-file` and `--library` CLI generator options removed, they're both now automatically detected by uniffi - Java callback interface implementations must now use primitive types (e.g., `int`, `long`, `boolean`) instead of boxed types (`Integer`, `Long`, `Boolean`) for non-optional primitive parameters and return types - use Java primitive types (`int`, `long`, `boolean`, etc.) instead of boxed types (`Integer`, `Long`, `Boolean`) for non-optional primitive parameters, return types, and record fields. Optional primitives and primitives in generic contexts (e.g., `List`, `CompletableFuture`) still use boxed types as required by Java. +- use primitive arrays where possible (similar to how `Vec -> bytes[]`). This should reduce GC pressure and speed up copies across the boundary via `AsBuffer` instead of iteration. There's a potentially small ergonomic hit to those doing a lot of data transformation, not just passing values to and from the Rust side. If you're someone this negatively impacts, reach out and we can talk about adding a `use_primitive_arrays` config option. ## 0.2.1 diff --git a/Cargo.lock b/Cargo.lock index 19b0d1d..24559c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1354,6 +1354,7 @@ dependencies = [ "uniffi-fixture-coverall", "uniffi-fixture-ext-types", "uniffi-fixture-futures", + "uniffi-fixture-primitive-arrays", "uniffi-fixture-proc-macro", "uniffi-fixture-rename", "uniffi-fixture-time", @@ -1485,6 +1486,13 @@ dependencies = [ "uniffi", ] +[[package]] +name = "uniffi-fixture-primitive-arrays" +version = "0.1.0" +dependencies = [ + "uniffi", +] + [[package]] name = "uniffi-fixture-proc-macro" version = "0.22.0" diff --git a/Cargo.toml b/Cargo.toml index e6d1f7c..c6631b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ uniffi-example-rondpoint = { git = "https://github.com/mozilla/uniffi-rs.git", t uniffi-fixture-coverall = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" } uniffi-fixture-ext-types = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" } uniffi-fixture-futures = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" } +uniffi-fixture-primitive-arrays = { path = "fixtures/primitive-arrays" } uniffi-fixture-proc-macro = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" } uniffi-fixture-rename = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" } uniffi-fixture-time = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" } diff --git a/fixtures/primitive-arrays/Cargo.toml b/fixtures/primitive-arrays/Cargo.toml new file mode 100644 index 0000000..c1191d8 --- /dev/null +++ b/fixtures/primitive-arrays/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "uniffi-fixture-primitive-arrays" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib", "lib"] +name = "uniffi_fixture_primitive_arrays" + +[dependencies] +uniffi = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" } diff --git a/fixtures/primitive-arrays/src/lib.rs b/fixtures/primitive-arrays/src/lib.rs new file mode 100644 index 0000000..3d6d16f --- /dev/null +++ b/fixtures/primitive-arrays/src/lib.rs @@ -0,0 +1,87 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +uniffi::setup_scaffolding!("primitive_arrays"); + +// Float32 (float[]) operations +#[uniffi::export] +fn roundtrip_float32(data: Vec) -> Vec { + data +} + +#[uniffi::export] +fn sum_float32(data: Vec) -> f32 { + data.iter().sum() +} + +// Float64 (double[]) operations +#[uniffi::export] +fn roundtrip_float64(data: Vec) -> Vec { + data +} + +#[uniffi::export] +fn sum_float64(data: Vec) -> f64 { + data.iter().sum() +} + +// Int16 (short[]) operations +#[uniffi::export] +fn roundtrip_int16(data: Vec) -> Vec { + data +} + +#[uniffi::export] +fn sum_int16(data: Vec) -> i16 { + data.iter().sum() +} + +// Int32 (int[]) operations +#[uniffi::export] +fn roundtrip_int32(data: Vec) -> Vec { + data +} + +#[uniffi::export] +fn sum_int32(data: Vec) -> i32 { + data.iter().sum() +} + +// Int64 (long[]) operations +#[uniffi::export] +fn roundtrip_int64(data: Vec) -> Vec { + data +} + +#[uniffi::export] +fn sum_int64(data: Vec) -> i64 { + data.iter().sum() +} + +// Boolean (boolean[]) operations +#[uniffi::export] +fn roundtrip_bool(data: Vec) -> Vec { + data +} + +#[uniffi::export] +fn count_true(data: Vec) -> i32 { + data.iter().filter(|&&b| b).count() as i32 +} + +// UInt variants to test unsigned handling +#[uniffi::export] +fn roundtrip_uint16(data: Vec) -> Vec { + data +} + +#[uniffi::export] +fn roundtrip_uint32(data: Vec) -> Vec { + data +} + +#[uniffi::export] +fn roundtrip_uint64(data: Vec) -> Vec { + data +} diff --git a/src/gen_java/compounds.rs b/src/gen_java/compounds.rs index 25ea2c6..68b96c8 100644 --- a/src/gen_java/compounds.rs +++ b/src/gen_java/compounds.rs @@ -109,3 +109,30 @@ impl CodeType for MapCodeType { ) } } + +// Primitive array types for sequences of primitives. +// These generate Java primitive arrays (e.g., float[], int[]) instead of List. + +macro_rules! impl_primitive_array_code_type { + ($name:ident, $type_label:literal, $canonical_name:literal) => { + #[derive(Debug)] + pub struct $name; + + impl CodeType for $name { + fn type_label(&self, _ci: &ComponentInterface, _config: &Config) -> String { + $type_label.into() + } + + fn canonical_name(&self) -> String { + $canonical_name.into() + } + } + }; +} + +impl_primitive_array_code_type!(Int16ArrayCodeType, "short[]", "Int16Array"); +impl_primitive_array_code_type!(Int32ArrayCodeType, "int[]", "Int32Array"); +impl_primitive_array_code_type!(Int64ArrayCodeType, "long[]", "Int64Array"); +impl_primitive_array_code_type!(Float32ArrayCodeType, "float[]", "Float32Array"); +impl_primitive_array_code_type!(Float64ArrayCodeType, "double[]", "Float64Array"); +impl_primitive_array_code_type!(BooleanArrayCodeType, "boolean[]", "BooleanArray"); diff --git a/src/gen_java/mod.rs b/src/gen_java/mod.rs index 23c20e1..7208757 100644 --- a/src/gen_java/mod.rs +++ b/src/gen_java/mod.rs @@ -635,9 +635,16 @@ impl AsCodeType for Type { Type::Optional { inner_type } => { Box::new(compounds::OptionalCodeType::new((*inner_type).clone())) } - Type::Sequence { inner_type } => { - Box::new(compounds::SequenceCodeType::new((*inner_type).clone())) - } + Type::Sequence { inner_type } => match inner_type.as_ref() { + Type::Int16 | Type::UInt16 => Box::new(compounds::Int16ArrayCodeType), + Type::Int32 | Type::UInt32 => Box::new(compounds::Int32ArrayCodeType), + Type::Int64 | Type::UInt64 => Box::new(compounds::Int64ArrayCodeType), + Type::Float32 => Box::new(compounds::Float32ArrayCodeType), + Type::Float64 => Box::new(compounds::Float64ArrayCodeType), + Type::Boolean => Box::new(compounds::BooleanArrayCodeType), + // Int8/UInt8 sequences still use SequenceCodeType; the separate Bytes type handles byte[] + _ => Box::new(compounds::SequenceCodeType::new((*inner_type).clone())), + }, Type::Map { key_type, value_type, @@ -744,10 +751,18 @@ mod filters { Type::Optional { inner_type } => { Ok(fully_qualified_type_label(inner_type, ci, config)?.to_string()) } - Type::Sequence { inner_type } => Ok(format!( - "java.util.List<{}>", - fully_qualified_type_label(inner_type, ci, config)? - )), + Type::Sequence { inner_type } => match inner_type.as_ref() { + Type::Int16 | Type::UInt16 => Ok("short[]".to_string()), + Type::Int32 | Type::UInt32 => Ok("int[]".to_string()), + Type::Int64 | Type::UInt64 => Ok("long[]".to_string()), + Type::Float32 => Ok("float[]".to_string()), + Type::Float64 => Ok("double[]".to_string()), + Type::Boolean => Ok("boolean[]".to_string()), + _ => Ok(format!( + "java.util.List<{}>", + fully_qualified_type_label(inner_type, ci, config)? + )), + }, Type::Map { key_type, value_type, @@ -1266,8 +1281,8 @@ mod tests { use super::*; use uniffi_bindgen::interface::ComponentInterface; use uniffi_meta::{ - EnumMetadata, EnumShape, FnMetadata, Metadata, MetadataGroup, NamespaceMetadata, Type, - VariantMetadata, + EnumMetadata, EnumShape, FnMetadata, FnParamMetadata, Metadata, MetadataGroup, + NamespaceMetadata, Type, VariantMetadata, }; #[test] @@ -1345,4 +1360,204 @@ mod tests { ); assert_eq!(JavaCodeOracle.convert_error_suffix("Error"), "Error"); } + + fn create_primitive_array_test_group() -> MetadataGroup { + let mut group = MetadataGroup { + namespace: NamespaceMetadata { + crate_name: "test".to_string(), + name: "test".to_string(), + ..Default::default() + }, + namespace_docstring: None, + items: Default::default(), + }; + + // Add functions for each primitive array type + let primitive_types = [ + ("process_floats", Type::Float32), + ("process_doubles", Type::Float64), + ("process_shorts", Type::Int16), + ("process_ints", Type::Int32), + ("process_longs", Type::Int64), + ("process_bools", Type::Boolean), + ]; + + for (name, inner_type) in primitive_types { + group.add_item(Metadata::Func(FnMetadata { + module_path: "test".to_string(), + name: name.to_string(), + is_async: false, + inputs: vec![FnParamMetadata { + name: "data".to_string(), + ty: Type::Sequence { + inner_type: Box::new(inner_type.clone()), + }, + by_ref: false, + optional: false, + default: None, + }], + return_type: Some(Type::Sequence { + inner_type: Box::new(inner_type), + }), + throws: None, + checksum: None, + docstring: None, + })); + } + + group + } + + #[test] + fn generates_float32_primitive_array() { + let group = create_primitive_array_test_group(); + let ci = ComponentInterface::from_metadata(group).unwrap(); + let bindings = generate_bindings(&Config::default(), &ci).unwrap(); + + assert!( + bindings.contains("float[]"), + "expected float[] in generated bindings" + ); + assert!( + bindings.contains("FfiConverterFloat32Array"), + "expected FfiConverterFloat32Array in generated bindings" + ); + assert!( + bindings.contains("public static float[] processFloats(float[] data)"), + "expected processFloats method with float[] signature" + ); + } + + #[test] + fn generates_float64_primitive_array() { + let group = create_primitive_array_test_group(); + let ci = ComponentInterface::from_metadata(group).unwrap(); + let bindings = generate_bindings(&Config::default(), &ci).unwrap(); + + assert!( + bindings.contains("double[]"), + "expected double[] in generated bindings" + ); + assert!( + bindings.contains("FfiConverterFloat64Array"), + "expected FfiConverterFloat64Array in generated bindings" + ); + assert!( + bindings.contains("public static double[] processDoubles(double[] data)"), + "expected processDoubles method with double[] signature" + ); + } + + #[test] + fn generates_int32_primitive_array() { + let group = create_primitive_array_test_group(); + let ci = ComponentInterface::from_metadata(group).unwrap(); + let bindings = generate_bindings(&Config::default(), &ci).unwrap(); + + assert!( + bindings.contains("int[]"), + "expected int[] in generated bindings" + ); + assert!( + bindings.contains("FfiConverterInt32Array"), + "expected FfiConverterInt32Array in generated bindings" + ); + assert!( + bindings.contains("public static int[] processInts(int[] data)"), + "expected processInts method with int[] signature" + ); + } + + #[test] + fn generates_int64_primitive_array() { + let group = create_primitive_array_test_group(); + let ci = ComponentInterface::from_metadata(group).unwrap(); + let bindings = generate_bindings(&Config::default(), &ci).unwrap(); + + assert!( + bindings.contains("long[]"), + "expected long[] in generated bindings" + ); + assert!( + bindings.contains("FfiConverterInt64Array"), + "expected FfiConverterInt64Array in generated bindings" + ); + assert!( + bindings.contains("public static long[] processLongs(long[] data)"), + "expected processLongs method with long[] signature" + ); + } + + #[test] + fn generates_int16_primitive_array() { + let group = create_primitive_array_test_group(); + let ci = ComponentInterface::from_metadata(group).unwrap(); + let bindings = generate_bindings(&Config::default(), &ci).unwrap(); + + assert!( + bindings.contains("short[]"), + "expected short[] in generated bindings" + ); + assert!( + bindings.contains("FfiConverterInt16Array"), + "expected FfiConverterInt16Array in generated bindings" + ); + assert!( + bindings.contains("public static short[] processShorts(short[] data)"), + "expected processShorts method with short[] signature" + ); + } + + #[test] + fn generates_boolean_primitive_array() { + let group = create_primitive_array_test_group(); + let ci = ComponentInterface::from_metadata(group).unwrap(); + let bindings = generate_bindings(&Config::default(), &ci).unwrap(); + + assert!( + bindings.contains("boolean[]"), + "expected boolean[] in generated bindings" + ); + assert!( + bindings.contains("FfiConverterBooleanArray"), + "expected FfiConverterBooleanArray in generated bindings" + ); + assert!( + bindings.contains("public static boolean[] processBools(boolean[] data)"), + "expected processBools method with boolean[] signature" + ); + } + + #[test] + fn does_not_generate_list_for_primitive_sequences() { + let group = create_primitive_array_test_group(); + let ci = ComponentInterface::from_metadata(group).unwrap(); + let bindings = generate_bindings(&Config::default(), &ci).unwrap(); + + // Verify that we don't generate boxed List types for primitives + assert!( + !bindings.contains("List"), + "should not contain List" + ); + assert!( + !bindings.contains("List"), + "should not contain List" + ); + assert!( + !bindings.contains("List"), + "should not contain List" + ); + assert!( + !bindings.contains("List"), + "should not contain List" + ); + assert!( + !bindings.contains("List"), + "should not contain List" + ); + assert!( + !bindings.contains("List"), + "should not contain List" + ); + } } diff --git a/src/templates/BooleanArrayHelper.java b/src/templates/BooleanArrayHelper.java new file mode 100644 index 0000000..dd82ab4 --- /dev/null +++ b/src/templates/BooleanArrayHelper.java @@ -0,0 +1,28 @@ +package {{ config.package_name() }}; + +public enum FfiConverterBooleanArray implements FfiConverterRustBuffer { + INSTANCE; + + @Override + public boolean[] read(java.nio.ByteBuffer buf) { + int len = buf.getInt(); + boolean[] arr = new boolean[len]; + for (int i = 0; i < len; i++) { + arr[i] = buf.get() != 0; + } + return arr; + } + + @Override + public long allocationSize(boolean[] value) { + return 4L + (long) value.length; + } + + @Override + public void write(boolean[] value, java.nio.ByteBuffer buf) { + buf.putInt(value.length); + for (boolean b : value) { + buf.put(b ? (byte) 1 : (byte) 0); + } + } +} diff --git a/src/templates/Float32ArrayHelper.java b/src/templates/Float32ArrayHelper.java new file mode 100644 index 0000000..7f1a5d3 --- /dev/null +++ b/src/templates/Float32ArrayHelper.java @@ -0,0 +1,26 @@ +package {{ config.package_name() }}; + +public enum FfiConverterFloat32Array implements FfiConverterRustBuffer { + INSTANCE; + + @Override + public float[] read(java.nio.ByteBuffer buf) { + int len = buf.getInt(); + float[] arr = new float[len]; + buf.asFloatBuffer().get(arr); + buf.position(buf.position() + len * 4); + return arr; + } + + @Override + public long allocationSize(float[] value) { + return 4L + (long) value.length * 4L; + } + + @Override + public void write(float[] value, java.nio.ByteBuffer buf) { + buf.putInt(value.length); + buf.asFloatBuffer().put(value); + buf.position(buf.position() + value.length * 4); + } +} diff --git a/src/templates/Float64ArrayHelper.java b/src/templates/Float64ArrayHelper.java new file mode 100644 index 0000000..738649d --- /dev/null +++ b/src/templates/Float64ArrayHelper.java @@ -0,0 +1,26 @@ +package {{ config.package_name() }}; + +public enum FfiConverterFloat64Array implements FfiConverterRustBuffer { + INSTANCE; + + @Override + public double[] read(java.nio.ByteBuffer buf) { + int len = buf.getInt(); + double[] arr = new double[len]; + buf.asDoubleBuffer().get(arr); + buf.position(buf.position() + len * 8); + return arr; + } + + @Override + public long allocationSize(double[] value) { + return 4L + (long) value.length * 8L; + } + + @Override + public void write(double[] value, java.nio.ByteBuffer buf) { + buf.putInt(value.length); + buf.asDoubleBuffer().put(value); + buf.position(buf.position() + value.length * 8); + } +} diff --git a/src/templates/Int16ArrayHelper.java b/src/templates/Int16ArrayHelper.java new file mode 100644 index 0000000..e54be84 --- /dev/null +++ b/src/templates/Int16ArrayHelper.java @@ -0,0 +1,26 @@ +package {{ config.package_name() }}; + +public enum FfiConverterInt16Array implements FfiConverterRustBuffer { + INSTANCE; + + @Override + public short[] read(java.nio.ByteBuffer buf) { + int len = buf.getInt(); + short[] arr = new short[len]; + buf.asShortBuffer().get(arr); + buf.position(buf.position() + len * 2); + return arr; + } + + @Override + public long allocationSize(short[] value) { + return 4L + (long) value.length * 2L; + } + + @Override + public void write(short[] value, java.nio.ByteBuffer buf) { + buf.putInt(value.length); + buf.asShortBuffer().put(value); + buf.position(buf.position() + value.length * 2); + } +} diff --git a/src/templates/Int32ArrayHelper.java b/src/templates/Int32ArrayHelper.java new file mode 100644 index 0000000..3daf093 --- /dev/null +++ b/src/templates/Int32ArrayHelper.java @@ -0,0 +1,26 @@ +package {{ config.package_name() }}; + +public enum FfiConverterInt32Array implements FfiConverterRustBuffer { + INSTANCE; + + @Override + public int[] read(java.nio.ByteBuffer buf) { + int len = buf.getInt(); + int[] arr = new int[len]; + buf.asIntBuffer().get(arr); + buf.position(buf.position() + len * 4); + return arr; + } + + @Override + public long allocationSize(int[] value) { + return 4L + (long) value.length * 4L; + } + + @Override + public void write(int[] value, java.nio.ByteBuffer buf) { + buf.putInt(value.length); + buf.asIntBuffer().put(value); + buf.position(buf.position() + value.length * 4); + } +} diff --git a/src/templates/Int64ArrayHelper.java b/src/templates/Int64ArrayHelper.java new file mode 100644 index 0000000..44188d4 --- /dev/null +++ b/src/templates/Int64ArrayHelper.java @@ -0,0 +1,26 @@ +package {{ config.package_name() }}; + +public enum FfiConverterInt64Array implements FfiConverterRustBuffer { + INSTANCE; + + @Override + public long[] read(java.nio.ByteBuffer buf) { + int len = buf.getInt(); + long[] arr = new long[len]; + buf.asLongBuffer().get(arr); + buf.position(buf.position() + len * 8); + return arr; + } + + @Override + public long allocationSize(long[] value) { + return 4L + (long) value.length * 8L; + } + + @Override + public void write(long[] value, java.nio.ByteBuffer buf) { + buf.putInt(value.length); + buf.asLongBuffer().put(value); + buf.position(buf.position() + value.length * 8); + } +} diff --git a/src/templates/Types.java b/src/templates/Types.java index c6bbc7b..330cbe6 100644 --- a/src/templates/Types.java +++ b/src/templates/Types.java @@ -153,7 +153,22 @@ private UniffiWithHandle() {} {% include "RecordTemplate.java" %} {%- when Type::Sequence { inner_type } %} +{%- match inner_type.as_ref() %} +{%- when Type::Int16 or Type::UInt16 %} +{%- include "Int16ArrayHelper.java" %} +{%- when Type::Int32 or Type::UInt32 %} +{%- include "Int32ArrayHelper.java" %} +{%- when Type::Int64 or Type::UInt64 %} +{%- include "Int64ArrayHelper.java" %} +{%- when Type::Float32 %} +{%- include "Float32ArrayHelper.java" %} +{%- when Type::Float64 %} +{%- include "Float64ArrayHelper.java" %} +{%- when Type::Boolean %} +{%- include "BooleanArrayHelper.java" %} +{%- else %} {% include "SequenceTemplate.java" %} +{%- endmatch %} {%- when Type::String %} {%- include "StringHelper.java" %} diff --git a/tests/scripts/TestFixtureCoverall.java b/tests/scripts/TestFixtureCoverall.java index f316b69..ef19959 100644 --- a/tests/scripts/TestFixtureCoverall.java +++ b/tests/scripts/TestFixtureCoverall.java @@ -301,11 +301,11 @@ public String getOption(String v, boolean arg2) throws ComplexException { } @Override - public java.util.List getList(java.util.List v, boolean arg2) { + public int[] getList(int[] v, boolean arg2) { if (arg2) { return v; } else { - return List.of(); + return new int[0]; } } @@ -586,8 +586,8 @@ static void testGettersFromJava(Getters getters) { throw new RuntimeException("Unexpected ComplexException", e); } - assert getters.getList(Arrays.asList(1, 2, 3), true).equals(Arrays.asList(1, 2, 3)); - assert getters.getList(Arrays.asList(1, 2, 3), false).equals(Arrays.asList()); + assert Arrays.equals(getters.getList(new int[]{1, 2, 3}, true), new int[]{1, 2, 3}); + assert Arrays.equals(getters.getList(new int[]{1, 2, 3}, false), new int[0]); // void function returns nothing, compiler won't let us write this test // assert getters.getNothing("hello") == Unit; diff --git a/tests/scripts/TestPrimitiveArrays.java b/tests/scripts/TestPrimitiveArrays.java new file mode 100644 index 0000000..f2dd079 --- /dev/null +++ b/tests/scripts/TestPrimitiveArrays.java @@ -0,0 +1,164 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import uniffi.primitive_arrays.*; +import java.util.Arrays; + +public class TestPrimitiveArrays { + public static void main(String[] args) { + testFloat32Arrays(); + testFloat64Arrays(); + testInt16Arrays(); + testInt32Arrays(); + testInt64Arrays(); + testBooleanArrays(); + testUnsignedArrays(); + testEmptyArrays(); + testLargeArrays(); + + System.out.println("All primitive array tests passed!"); + } + + static void testFloat32Arrays() { + // Test float[] roundtrip + float[] floats = new float[] { 1.0f, 2.5f, 3.14159f, -0.5f, Float.MAX_VALUE, Float.MIN_VALUE }; + float[] floatResult = PrimitiveArrays.roundtripFloat32(floats); + assert Arrays.equals(floats, floatResult) : "float[] roundtrip failed"; + + // Test sum + float[] sumInput = new float[] { 1.0f, 2.0f, 3.0f, 4.0f }; + float sum = PrimitiveArrays.sumFloat32(sumInput); + assert sum == 10.0f : "sumFloat32 failed: expected 10.0, got " + sum; + } + + static void testFloat64Arrays() { + // Test double[] roundtrip + double[] doubles = new double[] { 1.0, 2.5, 3.14159265359, -0.5, Double.MAX_VALUE, Double.MIN_VALUE }; + double[] doubleResult = PrimitiveArrays.roundtripFloat64(doubles); + assert Arrays.equals(doubles, doubleResult) : "double[] roundtrip failed"; + + // Test sum + double[] sumInput = new double[] { 1.0, 2.0, 3.0, 4.0, 5.0 }; + double sum = PrimitiveArrays.sumFloat64(sumInput); + assert sum == 15.0 : "sumFloat64 failed: expected 15.0, got " + sum; + } + + static void testInt16Arrays() { + // Test short[] roundtrip + short[] shorts = new short[] { -1, 0, 1, Short.MAX_VALUE, Short.MIN_VALUE, 12345 }; + short[] shortResult = PrimitiveArrays.roundtripInt16(shorts); + assert Arrays.equals(shorts, shortResult) : "short[] roundtrip failed"; + + // Test sum + short[] sumInput = new short[] { 1, 2, 3, 4, 5 }; + short sum = PrimitiveArrays.sumInt16(sumInput); + assert sum == 15 : "sumInt16 failed: expected 15, got " + sum; + } + + static void testInt32Arrays() { + // Test int[] roundtrip + int[] ints = new int[] { -1, 0, 1, Integer.MAX_VALUE, Integer.MIN_VALUE, 123456789 }; + int[] intResult = PrimitiveArrays.roundtripInt32(ints); + assert Arrays.equals(ints, intResult) : "int[] roundtrip failed"; + + // Test sum + int[] sumInput = new int[] { 1, 2, 3, 4, 5 }; + int sum = PrimitiveArrays.sumInt32(sumInput); + assert sum == 15 : "sumInt32 failed: expected 15, got " + sum; + } + + static void testInt64Arrays() { + // Test long[] roundtrip + long[] longs = new long[] { -1L, 0L, 1L, Long.MAX_VALUE, Long.MIN_VALUE, 123456789012345L }; + long[] longResult = PrimitiveArrays.roundtripInt64(longs); + assert Arrays.equals(longs, longResult) : "long[] roundtrip failed"; + + // Test sum + long[] sumInput = new long[] { 1L, 2L, 3L, 4L, 5L }; + long sum = PrimitiveArrays.sumInt64(sumInput); + assert sum == 15L : "sumInt64 failed: expected 15, got " + sum; + } + + static void testBooleanArrays() { + // Test boolean[] roundtrip + boolean[] bools = new boolean[] { true, false, true, true, false, false, true }; + boolean[] boolResult = PrimitiveArrays.roundtripBool(bools); + assert Arrays.equals(bools, boolResult) : "boolean[] roundtrip failed"; + + // Test count_true + int trueCount = PrimitiveArrays.countTrue(bools); + assert trueCount == 4 : "countTrue failed: expected 4, got " + trueCount; + + // Test all false + boolean[] allFalse = new boolean[] { false, false, false }; + assert PrimitiveArrays.countTrue(allFalse) == 0 : "countTrue for all false failed"; + + // Test all true + boolean[] allTrue = new boolean[] { true, true, true }; + assert PrimitiveArrays.countTrue(allTrue) == 3 : "countTrue for all true failed"; + } + + static void testUnsignedArrays() { + // Test unsigned short[] (u16) - values that would be negative as signed + short[] uint16Values = new short[] { 0, 1, (short) 32767, (short) 32768, (short) 65535 }; + short[] uint16Result = PrimitiveArrays.roundtripUint16(uint16Values); + assert Arrays.equals(uint16Values, uint16Result) : "u16 as short[] roundtrip failed"; + + // Test unsigned int[] (u32) - values that would be negative as signed + int[] uint32Values = new int[] { 0, 1, Integer.MAX_VALUE, Integer.MAX_VALUE + 1, -1 }; + int[] uint32Result = PrimitiveArrays.roundtripUint32(uint32Values); + assert Arrays.equals(uint32Values, uint32Result) : "u32 as int[] roundtrip failed"; + + // Test unsigned long[] (u64) - values that would be negative as signed + long[] uint64Values = new long[] { 0L, 1L, Long.MAX_VALUE, Long.MAX_VALUE + 1, -1L }; + long[] uint64Result = PrimitiveArrays.roundtripUint64(uint64Values); + assert Arrays.equals(uint64Values, uint64Result) : "u64 as long[] roundtrip failed"; + } + + static void testEmptyArrays() { + // Test empty arrays for all types + assert PrimitiveArrays.roundtripFloat32(new float[0]).length == 0 : "empty float[] roundtrip failed"; + assert PrimitiveArrays.roundtripFloat64(new double[0]).length == 0 : "empty double[] roundtrip failed"; + assert PrimitiveArrays.roundtripInt16(new short[0]).length == 0 : "empty short[] roundtrip failed"; + assert PrimitiveArrays.roundtripInt32(new int[0]).length == 0 : "empty int[] roundtrip failed"; + assert PrimitiveArrays.roundtripInt64(new long[0]).length == 0 : "empty long[] roundtrip failed"; + assert PrimitiveArrays.roundtripBool(new boolean[0]).length == 0 : "empty boolean[] roundtrip failed"; + + // Test sums on empty arrays + assert PrimitiveArrays.sumFloat32(new float[0]) == 0.0f : "sumFloat32 on empty array failed"; + assert PrimitiveArrays.sumFloat64(new double[0]) == 0.0 : "sumFloat64 on empty array failed"; + assert PrimitiveArrays.sumInt32(new int[0]) == 0 : "sumInt32 on empty array failed"; + assert PrimitiveArrays.countTrue(new boolean[0]) == 0 : "countTrue on empty array failed"; + } + + static void testLargeArrays() { + // Test large arrays (10,000 elements) for performance sanity + int size = 10000; + + // Float32 + float[] largeFloats = new float[size]; + for (int i = 0; i < size; i++) { + largeFloats[i] = (float) i * 0.1f; + } + float[] largeFloatResult = PrimitiveArrays.roundtripFloat32(largeFloats); + assert Arrays.equals(largeFloats, largeFloatResult) : "large float[] roundtrip failed"; + + // Int32 + int[] largeInts = new int[size]; + for (int i = 0; i < size; i++) { + largeInts[i] = i; + } + int[] largeIntResult = PrimitiveArrays.roundtripInt32(largeInts); + assert Arrays.equals(largeInts, largeIntResult) : "large int[] roundtrip failed"; + + // Boolean + boolean[] largeBools = new boolean[size]; + for (int i = 0; i < size; i++) { + largeBools[i] = i % 2 == 0; + } + boolean[] largeBoolResult = PrimitiveArrays.roundtripBool(largeBools); + assert Arrays.equals(largeBools, largeBoolResult) : "large boolean[] roundtrip failed"; + assert PrimitiveArrays.countTrue(largeBools) == size / 2 : "countTrue on large array failed"; + } +} diff --git a/tests/tests.rs b/tests/tests.rs index 4b19efc..82b1ece 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -269,4 +269,5 @@ fixture_tests! { (test_omit_checksums, "uniffi-example-arithmetic", "scripts/TestOmitChecksums/TestOmitChecksums.java"), (test_proc_macro, "uniffi-fixture-proc-macro", "scripts/TestProcMacro.java"), (test_rename, "uniffi-fixture-rename", "scripts/TestRename/TestRename.java"), + (test_primitive_arrays, "uniffi-fixture-primitive-arrays", "scripts/TestPrimitiveArrays.java"), }