diff --git a/Cargo.lock b/Cargo.lock index 6c6f34ba712..f499fc64377 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -841,6 +841,16 @@ dependencies = [ "serde_json", ] +[[package]] +name = "icu_capi" +version = "0.1.0" +dependencies = [ + "icu_locid", + "icu_plurals", + "icu_provider", + "icu_provider_fs", +] + [[package]] name = "icu_datetime" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 8f8f539ae97..6b0f9ac1e63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "experimental/segmenter_lstm", "components/datetime", "components/ecma402", + "components/capi", "components/icu", "components/icu4x", "components/locale_canonicalizer", diff --git a/Makefile.toml b/Makefile.toml index d56753eb723..faf69686fd1 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -17,6 +17,14 @@ command = "cargo" # Note: we need test-all-features (rather than build-all-features) for docs args = ["test-all-features"] +[tasks.test-capi] +description = "Run C API tests" +category = "ICU4X Development" +script = ''' +cd components/capi/examples/pluralrules; +make +''' + [tasks.license-header-check] description = "Ensure all the source files have license headers" category = "ICU4X Development" @@ -56,6 +64,7 @@ dependencies = [ "fmt-check", "clippy-all", "license-header-check", + "test-capi", ] [tasks.ci] diff --git a/components/capi/Cargo.toml b/components/capi/Cargo.toml new file mode 100644 index 00000000000..bad26529c7c --- /dev/null +++ b/components/capi/Cargo.toml @@ -0,0 +1,20 @@ +# This file is part of ICU4X. For terms of use, please see the file +# called LICENSE at the top level of the ICU4X source tree +# (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +[package] +name = "icu_capi" +version = "0.1.0" +authors = ["The ICU4X Developers"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["staticlib", "rlib"] + +[dependencies] +icu_locid = { path = "../locid" } +icu_plurals = { path = "../plurals/" } +icu_provider = { path = "../provider" } +icu_provider_fs = { path = "../provider_fs/" } diff --git a/components/capi/examples/pluralrules/.gitignore b/components/capi/examples/pluralrules/.gitignore new file mode 100644 index 00000000000..f8305e747aa --- /dev/null +++ b/components/capi/examples/pluralrules/.gitignore @@ -0,0 +1 @@ +a.out \ No newline at end of file diff --git a/components/capi/examples/pluralrules/Makefile b/components/capi/examples/pluralrules/Makefile new file mode 100644 index 00000000000..c9b10207697 --- /dev/null +++ b/components/capi/examples/pluralrules/Makefile @@ -0,0 +1,25 @@ +# This file is part of ICU4X. For terms of use, please see the file +# called LICENSE at the top level of the ICU4X source tree +# (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +.DEFAULT_GOAL := test +.PHONY: build test + +ALL_HEADERS := $(wildcard ../../include/*.h) +ALL_RUST := $(wildcard ../../src/*.rs) + +$(ALL_RUST): + +$(ALL_HEADERS): + + +../../../../target/debug/libicu_capi.a: $(ALL_RUST) + cargo build + +a.out: ../../../../target/debug/libicu_capi.a $(ALL_HEADERS) test.c + gcc test.c ../../../../target/debug/libicu_capi.a -ldl -lpthread -lm + +build: a.out + +test: build + ./a.out \ No newline at end of file diff --git a/components/capi/examples/pluralrules/test.c b/components/capi/examples/pluralrules/test.c new file mode 100644 index 00000000000..89d75a1172b --- /dev/null +++ b/components/capi/examples/pluralrules/test.c @@ -0,0 +1,40 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +#include "../../include/pluralrules.h" +#include +#include + +const char* path = "../../../../resources/testdata/data/json/"; +int main() { + ICU4XLocale* locale = icu4x_locale_create("ar", 2); + ICU4XCreateDataProviderResult result = icu4x_fs_data_provider_create(path, strlen(path)); + if (!result.success) { + printf("Failed to create FsDataProvider\n"); + return 1; + } + ICU4XDataProvider provider = result.provider; + ICU4XCreatePluralRulesResult plural_result = icu4x_plural_rules_create(locale, &provider, ICU4XPluralRuleType_Cardinal); + if (!plural_result.success) { + printf("Failed to create PluralRules\n"); + return 1; + } + ICU4XPluralRules* rules = plural_result.rules; + + ICU4XPluralOperands op; + op.i = 3; + + ICU4XPluralCategory cat = icu4x_plural_rules_select(rules, &op); + + printf("Plural Category %d (should be %d)\n", (int)cat, (int)ICU4XPluralCategory_Few); + + icu4x_plural_rules_destroy(rules); + icu4x_data_provider_destroy(provider); + icu4x_locale_destroy(locale); + + if (cat != ICU4XPluralCategory_Few) { + return 1; + } + return 0; +} diff --git a/components/capi/include/locale.h b/components/capi/include/locale.h new file mode 100644 index 00000000000..b1dd78bb897 --- /dev/null +++ b/components/capi/include/locale.h @@ -0,0 +1,18 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +#ifndef ICU4X_LOCALE_H +#define ICU4X_LOCALE_H + +#include +#include +#include + +// opaque +typedef struct ICU4XLocale ICU4XLocale; + +ICU4XLocale* icu4x_locale_create(const char* value, size_t len); +void icu4x_locale_destroy(ICU4XLocale*); + +#endif // ICU4X_LOCALE_H \ No newline at end of file diff --git a/components/capi/include/pluralrules.h b/components/capi/include/pluralrules.h new file mode 100644 index 00000000000..ef45a606022 --- /dev/null +++ b/components/capi/include/pluralrules.h @@ -0,0 +1,50 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +#ifndef ICU4X_PLURALRULES_H +#define ICU4X_PLURALRULES_H + +#include +#include +#include +#include "provider.h" +#include "locale.h" + +// opaque +typedef struct ICU4XPluralRules ICU4XPluralRules; + +typedef struct { + ICU4XPluralRules* rules; + bool success; +} ICU4XCreatePluralRulesResult; + +typedef enum { + ICU4XPluralRuleType_Cardinal, + ICU4XPluralRuleType_Ordinal +} ICU4XPluralRuleType; + +typedef enum { + ICU4XPluralCategory_Zero, + ICU4XPluralCategory_One, + ICU4XPluralCategory_Two, + ICU4XPluralCategory_Few, + ICU4XPluralCategory_Many, + ICU4XPluralCategory_Other, +} ICU4XPluralCategory; + +typedef struct { + uint64_t i; + size_t v; + size_t w; + uint64_t f; + uint64_t t; + size_t c; +} ICU4XPluralOperands; + + +ICU4XCreatePluralRulesResult icu4x_plural_rules_create(const ICU4XLocale* locale, const ICU4XDataProvider* provider, ICU4XPluralRuleType ty); +ICU4XPluralCategory icu4x_plural_rules_select(const ICU4XPluralRules* rules, const ICU4XPluralOperands* op); +void icu4x_plural_rules_destroy(ICU4XPluralRules* rules); + +#endif // ICU4X_PLURALRULES_H \ No newline at end of file diff --git a/components/capi/include/provider.h b/components/capi/include/provider.h new file mode 100644 index 00000000000..5bf84e145c2 --- /dev/null +++ b/components/capi/include/provider.h @@ -0,0 +1,26 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +#ifndef ICU4X_PROVIDER_H +#define ICU4X_PROVIDER_H + +#include +#include +#include + +typedef struct { + uintptr_t _field1; + uintptr_t _field2; +} ICU4XDataProvider; + +typedef struct { + ICU4XDataProvider provider; + bool success; +} ICU4XCreateDataProviderResult; + +void icu4x_data_provider_destroy(ICU4XDataProvider d); + +ICU4XCreateDataProviderResult icu4x_fs_data_provider_create(const char* path, size_t len); + +#endif // ICU4X_PROVIDER_H \ No newline at end of file diff --git a/components/capi/src/lib.rs b/components/capi/src/lib.rs new file mode 100644 index 00000000000..3a212d0887d --- /dev/null +++ b/components/capi/src/lib.rs @@ -0,0 +1,7 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +pub mod locale; +pub mod pluralrules; +pub mod provider; diff --git a/components/capi/src/locale.rs b/components/capi/src/locale.rs new file mode 100644 index 00000000000..abb95279ce4 --- /dev/null +++ b/components/capi/src/locale.rs @@ -0,0 +1,37 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +use std::slice; + +use icu_locid::Locale; + +/// Opaque type for use behind a pointer, is [`Locale`] +/// +/// Can be obtained via [`icu4x_locale_create()`] and destroyed via [`icu4x_locale_destroy()`] +pub type ICU4XLocale = Locale; + +#[no_mangle] +/// FFI version of [`Locale::from_bytes()`], see its docs for more details +/// +/// # Safety +/// `value` and `len` should point to a valid ASCII string of length `len`. +/// +/// It does not need to be be null terminated, and `len` should not include a null +/// terminator (this will just cause the function to panic, and is not a safety requirement). +pub unsafe extern "C" fn icu4x_locale_create(value: *const u8, len: usize) -> *mut ICU4XLocale { + let bytes = slice::from_raw_parts(value, len); + // todo: return errors + let loc = ICU4XLocale::from_bytes(bytes).unwrap(); + Box::into_raw(Box::new(loc)) +} + +#[no_mangle] +/// Destructor for [`ICU4XLocale`]. +/// +/// # Safety +/// +/// `loc` must be a pointer to a locale allocated by `icu4x_locale_destroy`. +pub unsafe extern "C" fn icu4x_locale_destroy(loc: *mut ICU4XLocale) { + let _ = Box::from_raw(loc); +} diff --git a/components/capi/src/pluralrules.rs b/components/capi/src/pluralrules.rs new file mode 100644 index 00000000000..86965cf3fd1 --- /dev/null +++ b/components/capi/src/pluralrules.rs @@ -0,0 +1,151 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +use icu_locid::Locale as ICULocale; +use icu_plurals::{PluralCategory, PluralOperands, PluralRuleType, PluralRules}; + +use crate::provider::ICU4XDataProvider; + +use std::ptr; + +/// Opaque type for use behind a pointer, is [`PluralRules`] +/// +/// Can be obtained via [`icu4x_plural_rules_create()`] and destroyed via [`icu4x_plural_rules_destroy()`] +pub type ICU4XPluralRules = PluralRules; + +#[repr(C)] +/// This is the result returned by [`icu4x_plural_rules_create()`] +pub struct ICU4XCreatePluralRulesResult { + /// Will be null if `success` is `false` + pub rules: *mut ICU4XPluralRules, + /// Currently just a boolean, but we might add a proper error enum + /// as necessary + pub success: bool, +} + +#[no_mangle] +/// FFI version of [`PluralRules::try_new()`] see its docs for more details +/// +/// # Safety +/// - `locale` should be constructed via [`icu4x_locale_create()`](crate::locale::icu4x_locale_create) +/// - `provider` should be constructed via one of the functions in [`crate::locale`](crate::locale) +/// - Only access `rules` in the result if `success` is true. +pub extern "C" fn icu4x_plural_rules_create( + locale: &ICULocale, + provider: &ICU4XDataProvider, + ty: ICU4XPluralRuleType, +) -> ICU4XCreatePluralRulesResult { + // cheap as long as there are no variants + let langid = locale.as_ref().clone(); + let provider = provider.as_dyn_ref(); + match ICU4XPluralRules::try_new(langid, provider, ty.into()) { + Ok(pr) => { + let pr = Box::new(pr); + ICU4XCreatePluralRulesResult { + rules: Box::into_raw(pr), + success: true, + } + } + Err(_) => ICU4XCreatePluralRulesResult { + rules: ptr::null_mut(), + success: false, + }, + } +} + +#[no_mangle] +/// FFI version of [`PluralRules::select()`], see its docs for more details +pub extern "C" fn icu4x_plural_rules_select( + pr: &ICU4XPluralRules, + op: &ICU4XPluralOperands, +) -> ICU4XPluralCategory { + pr.select(*op).into() +} + +#[no_mangle] +/// Destructor for [`ICU4XPluralRules`] +/// +/// # Safety +/// `pr` must be a pointer to a valid [`ICU4XPluralRules`] constructed by +/// [`icu4x_plural_rules_create()`]. +pub unsafe extern "C" fn icu4x_plural_rules_destroy(pr: *mut ICU4XPluralRules) { + let _ = Box::from_raw(pr); +} + +#[repr(C)] +#[derive(Copy, Clone)] +/// FFI version of [`PluralOperands`], see its docs for more details +pub struct ICU4XPluralOperands { + pub i: u64, + pub v: usize, + pub w: usize, + pub f: u64, + pub t: u64, + pub c: usize, +} + +#[repr(C)] +/// FFI version of [`PluralRuleType`], see its docs for more details +pub enum ICU4XPluralRuleType { + Cardinal, + Ordinal, +} + +#[repr(C)] +/// FFI version of [`PluralCategory`], see its docs for more details +pub enum ICU4XPluralCategory { + Zero, + One, + Two, + Few, + Many, + Other, +} + +impl From for ICU4XPluralOperands { + fn from(other: PluralOperands) -> Self { + Self { + i: other.i, + v: other.v, + w: other.w, + f: other.f, + t: other.t, + c: other.c, + } + } +} + +impl From for PluralOperands { + fn from(other: ICU4XPluralOperands) -> PluralOperands { + PluralOperands { + i: other.i, + v: other.v, + w: other.w, + f: other.f, + t: other.t, + c: other.c, + } + } +} + +impl From for PluralRuleType { + fn from(other: ICU4XPluralRuleType) -> Self { + match other { + ICU4XPluralRuleType::Cardinal => PluralRuleType::Cardinal, + ICU4XPluralRuleType::Ordinal => PluralRuleType::Ordinal, + } + } +} +impl From for ICU4XPluralCategory { + fn from(other: PluralCategory) -> Self { + match other { + PluralCategory::Zero => ICU4XPluralCategory::Zero, + PluralCategory::One => ICU4XPluralCategory::One, + PluralCategory::Two => ICU4XPluralCategory::Two, + PluralCategory::Few => ICU4XPluralCategory::Few, + PluralCategory::Many => ICU4XPluralCategory::Many, + PluralCategory::Other => ICU4XPluralCategory::Other, + } + } +} diff --git a/components/capi/src/provider.rs b/components/capi/src/provider.rs new file mode 100644 index 00000000000..322bfeb4a34 --- /dev/null +++ b/components/capi/src/provider.rs @@ -0,0 +1,122 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +use icu_provider::erased::ErasedDataProvider; +use icu_provider_fs::FsDataProvider; +use std::{mem, ptr, slice, str}; + +#[repr(C)] +/// FFI version of [`ErasedDataProvider`]. See its docs for more details. +/// +/// # Safety +/// +/// This should only be constructed in Rust via [`ICU4XDataProvider::from_boxed()`], or, +/// from the C side, via functions like [`icu4x_fs_data_provider_create()`]. +/// +/// This can be constructed by the functions in this module like [`icu4x_fs_data_provider_create()`], +/// and must be destroyed by [`icu4x_data_provider_destroy()`]. +pub struct ICU4XDataProvider { + /// Dummy fields to ensure this is the size of a trait object pointer + /// Can be improved once the Metadata API stabilizes + _field1: usize, + _field2: usize, +} + +impl ICU4XDataProvider { + /// This is unsafe because zeroed() can be passed to other functions + /// and cause UB + /// + /// This is necessary for returning uninitialized values to C. + /// + /// # Safety + /// + /// Only call for values that are to be returned to C and never passed to Rust. + pub unsafe fn zeroed() -> Self { + ICU4XDataProvider { + _field1: 0, + _field2: 0, + } + } + + /// Construct a [`ICU4XDataProvider`] this from a boxed [`ErasedDataProvider`] + pub fn from_boxed(x: Box>) -> Self { + unsafe { + // If the layout changes this will error + // Once Rust gets pointer metadata APIs we should switch to using those + mem::transmute(x) + } + } + + /// Obtain the original boxed Rust [`ErasedDataProvider`] for this + pub fn into_boxed(self) -> Box> { + debug_assert!(self._field1 != 0); + // If the layout changes this will error + // Once Rust gets pointer metadata APIs we should switch to using those + unsafe { mem::transmute(self) } + } + + /// Convert a borrowed reference to a borrowed [`ErasedDataProvider`] + pub fn as_dyn_ref(&self) -> &dyn ErasedDataProvider<'static> { + debug_assert!(self._field1 != 0); + unsafe { + // &dyn Trait and Box have the same layout + // Note that we are reading from a *pointer* to `Box`, + // so we need to `ptr::read` the fat pointer first. + let borrowed_erased: ICU4XDataProvider = ptr::read(self); + // If the layout changes this will error + // Once Rust gets pointer metadata APIs we should switch to using those + mem::transmute(borrowed_erased) + } + } +} + +#[no_mangle] +/// Destructor for [`ICU4XDataProvider`]. +/// +/// # Safety +/// +/// Must be used with a valid [`ICU4XDataProvider`] constructed by functions like +/// [`icu4x_fs_data_provider_create()`] +pub unsafe extern "C" fn icu4x_data_provider_destroy(d: ICU4XDataProvider) { + let _ = d.into_boxed(); +} + +#[repr(C)] +/// A result type for [`icu4x_fs_data_provider_create`]. +pub struct ICU4XCreateDataProviderResult { + /// Will be zeroed if `success` is `false`, do not use in that case + pub provider: ICU4XDataProvider, + // May potentially add a better error type in the future + pub success: bool, +} + +#[no_mangle] +/// Constructs an [`FsDataProvider`] and retirns it as an [`ICU4XDataProvider`]. +/// See [`FsDataProvider::try_new()`] for more details. +/// +/// # Safety +/// +/// `path` and `len` must point to a valid UTF-8 string, with `len` not including +/// a null terminator if any. +/// +/// Only access `provider` in the result if `success` is true. +pub unsafe extern "C" fn icu4x_fs_data_provider_create( + path: *const u8, + len: usize, +) -> ICU4XCreateDataProviderResult { + let path = str::from_utf8_unchecked(slice::from_raw_parts(path, len)); + match FsDataProvider::try_new(path.to_string()) { + Ok(fs) => { + let erased = Box::new(fs); + ICU4XCreateDataProviderResult { + provider: ICU4XDataProvider::from_boxed(erased), + success: true, + } + } + Err(_) => ICU4XCreateDataProviderResult { + provider: ICU4XDataProvider::zeroed(), + success: false, + }, + } +}