Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ C++ wrappers (using Crubit) for Rust libraries.
| saphyr | [saphyr](https://crates.io/crates/saphyr) | Compiles |
| serde_json | [serde_json](https://crates.io/crates/serde_json) | Compiles |
| zip | [zip](https://crates.io/crates/zip) | Compiles |
<!-- keep-sorted end -->
| leveldb | [rusty_leveldb](https://crates.io/crates/rusty_leveldb) | Doesn't compile - NOTE: Retry after `cc_std::virtual_unique_ptr` is supported |
| stemming | [snowball](https://crates.io/crates/snowball) | Compiles |

## Contributing

Expand Down
146 changes: 146 additions & 0 deletions stemming/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
cmake_minimum_required(VERSION 3.22)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_C_COMPILER clang)
set(CMAKE_CXX_COMPILER clang++)

project(stemming
VERSION 0.1.0
LANGUAGES CXX C)

include(FetchContent)

# Fetch snowball source to get algorithms
FetchContent_Declare(
snowball_repo
GIT_REPOSITORY https://github.com/snowballstem/snowball.git
GIT_TAG main
)
FetchContent_GetProperties(snowball_repo)
if(NOT snowball_repo_POPULATED)
FetchContent_Populate(snowball_repo)
endif()

# Rename main.rs to lib.rs to make it a library crate
if(EXISTS "${snowball_repo_SOURCE_DIR}/rust/src/main.rs")
file(RENAME "${snowball_repo_SOURCE_DIR}/rust/src/main.rs" "${snowball_repo_SOURCE_DIR}/rust/src/lib.rs")
endif()

# 1. Build snowball_compiler
file(GLOB COMPILER_SRCS "${snowball_repo_SOURCE_DIR}/compiler/*.c")
add_executable(snowball_compiler ${COMPILER_SRCS})
target_include_directories(snowball_compiler PRIVATE "${snowball_repo_SOURCE_DIR}/compiler")

# 2. Find all .sbl files
file(GLOB SBL_FILES "${snowball_repo_SOURCE_DIR}/algorithms/*.sbl")

# Extract language names
set(LANG_NAMES "")
foreach(SBL_FILE ${SBL_FILES})
get_filename_component(LANG_NAME ${SBL_FILE} NAME_WE)
list(APPEND LANG_NAMES ${LANG_NAME})
endforeach()

# Sort alphabetically (matches bash sort)
list(SORT LANG_NAMES)

# 3. Generate Rust algorithms
set(RUST_ALGORITHMS_DIR "${snowball_repo_SOURCE_DIR}/rust/src/snowball/algorithms")
file(MAKE_DIRECTORY ${RUST_ALGORITHMS_DIR})

set(GENERATED_RUST_ALGORITHM_FILES "")
foreach(LANG_NAME ${LANG_NAMES})
add_custom_command(
OUTPUT "${RUST_ALGORITHMS_DIR}/${LANG_NAME}_stemmer.rs"
COMMAND snowball_compiler "${snowball_repo_SOURCE_DIR}/algorithms/${LANG_NAME}.sbl" -rust -o "${RUST_ALGORITHMS_DIR}/${LANG_NAME}_stemmer"
DEPENDS snowball_compiler "${snowball_repo_SOURCE_DIR}/algorithms/${LANG_NAME}.sbl"
COMMENT "Generating Rust code for ${LANG_NAME} stemmer"
)
list(APPEND GENERATED_RUST_ALGORITHM_FILES "${RUST_ALGORITHMS_DIR}/${LANG_NAME}_stemmer.rs")
endforeach()

# Custom target to drive generation
add_custom_target(generate_rust_algorithms
DEPENDS ${GENERATED_RUST_ALGORITHM_FILES}
)

# 4. Generate generated_language_list.rs for OUR crate
set(ALGORITHM_NAMES_CONTENT "")
set(ALGORITHM_CSTR_CONTENT "")

foreach(LANG_NAME ${LANG_NAMES})
string(APPEND ALGORITHM_NAMES_CONTENT " \"${LANG_NAME}\",\n")
string(APPEND ALGORITHM_CSTR_CONTENT " c\"${LANG_NAME}\".as_ptr(),\n")
endforeach()

set(GENERATED_FILE_CONTENT
"// Generated by CMake. Do not edit.
const ALGORITHM_NAMES: &[&str] = &[
${ALGORITHM_NAMES_CONTENT}];

const ALGORITHM_NAMES_CSTR: &[*const std::os::raw::c_char] = &[
${ALGORITHM_CSTR_CONTENT} std::ptr::null(),
];
"
)

# Write to the source directory
file(WRITE "${CMAKE_CURRENT_SOURCE_DIR}/generated_language_list.rs" "${GENERATED_FILE_CONTENT}")

# 5. Generate Cargo.toml for OUR crate
# Copy LICENSE first
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../LICENSE")
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/../LICENSE" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")
endif()

set(CARGO_TOML_CONTENT
"[package]
name = \"rust\"
version = \"0.1.0\"
edition = \"2021\"
license-file = \"../LICENSE\"

[lib]
path = \"${CMAKE_CURRENT_SOURCE_DIR}/libstemmer.rs\"
crate-type = [\"staticlib\"]
doctest = false

[dependencies]
isolang = \"2.0\"
snowball_stemmer = { package = \"testapp\", path = \"${snowball_repo_SOURCE_DIR}/rust\" }
"
)

file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/rust/Cargo.toml" "${CARGO_TOML_CONTENT}")

# 6. Corrosion Setup
FetchContent_Declare(
Corrosion
GIT_REPOSITORY https://github.com/thunderseethe/corrosion
GIT_TAG master
)

FetchContent_MakeAvailable(Corrosion)

corrosion_import_crate(MANIFEST_PATH ${CMAKE_CURRENT_BINARY_DIR}/rust/Cargo.toml)

# Ensure algorithms are generated before building the Rust crate
add_dependencies(rust generate_rust_algorithms)
if(TARGET cargo-prebuild_rust)
add_dependencies(cargo-prebuild_rust generate_rust_algorithms)
endif()

FetchContent_Declare(
absl
GIT_REPOSITORY https://github.com/abseil/abseil-cpp.git
FIND_PACKAGE_ARGS NAMES absl
)
FetchContent_MakeAvailable(absl)

add_library(snowball_stemmer_wrap snowball_stemmer_wrap.cc snowball_stemmer_wrap.h)
target_link_libraries(snowball_stemmer_wrap PRIVATE rust absl::strings)
target_include_directories(snowball_stemmer_wrap PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}")

add_executable(stemming_example main.cc)
target_link_libraries(stemming_example PRIVATE snowball_stemmer_wrap absl::strings)
13 changes: 13 additions & 0 deletions stemming/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Stemming

Wrapper for the [snowball](https://crates.io/crates/snowball) crate.

## Usage

Requires installation of clang and a nightly version of Rust.

```
cd stemming
cmake -B build
cmake --build build --parallel
```
76 changes: 76 additions & 0 deletions stemming/generated_language_list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
const ALGORITHM_NAMES: &[&str] = &[
"arabic",
"armenian",
"basque",
"catalan",
"czech",
"danish",
"dutch",
"dutch_porter",
"english",
"esperanto",
"estonian",
"finnish",
"french",
"german",
"greek",
"hindi",
"hungarian",
"indonesian",
"irish",
"italian",
"lithuanian",
"lovins",
"nepali",
"norwegian",
"polish",
"porter",
"portuguese",
"romanian",
"russian",
"serbian",
"spanish",
"swedish",
"tamil",
"turkish",
"yiddish",
];

const ALGORITHM_NAMES_CSTR: &[*const std::os::raw::c_char] = &[
c"arabic".as_ptr(),
c"armenian".as_ptr(),
c"basque".as_ptr(),
c"catalan".as_ptr(),
c"czech".as_ptr(),
c"danish".as_ptr(),
c"dutch".as_ptr(),
c"dutch_porter".as_ptr(),
c"english".as_ptr(),
c"esperanto".as_ptr(),
c"estonian".as_ptr(),
c"finnish".as_ptr(),
c"french".as_ptr(),
c"german".as_ptr(),
c"greek".as_ptr(),
c"hindi".as_ptr(),
c"hungarian".as_ptr(),
c"indonesian".as_ptr(),
c"irish".as_ptr(),
c"italian".as_ptr(),
c"lithuanian".as_ptr(),
c"lovins".as_ptr(),
c"nepali".as_ptr(),
c"norwegian".as_ptr(),
c"polish".as_ptr(),
c"porter".as_ptr(),
c"portuguese".as_ptr(),
c"romanian".as_ptr(),
c"russian".as_ptr(),
c"serbian".as_ptr(),
c"spanish".as_ptr(),
c"swedish".as_ptr(),
c"tamil".as_ptr(),
c"turkish".as_ptr(),
c"yiddish".as_ptr(),
std::ptr::null(),
];
27 changes: 27 additions & 0 deletions stemming/libstemmer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#ifndef SECURITY_STEMMING_LIBSTEMMER_H_
#define SECURITY_STEMMING_LIBSTEMMER_H_

namespace snowball_stemmer_c {

struct sb_stemmer;

using sb_symbol = char;

extern "C" {

void sb_stemmer_delete(sb_stemmer *stemmer);

int sb_stemmer_length(sb_stemmer *stemmer);

const char **sb_stemmer_list();

sb_stemmer *sb_stemmer_new(const char *algorithm, const char *charenc);

const sb_symbol *sb_stemmer_stem(sb_stemmer *stemmer, const sb_symbol *word,
int size);

} // extern "C"

} // namespace snowball_stemmer_c

#endif // SECURITY_STEMMING_LIBSTEMMER_H_
123 changes: 123 additions & 0 deletions stemming/libstemmer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
use snowball_stemmer::Stemmer;
use std::ffi::CStr;
use std::os::raw::c_char;
use std::os::raw::c_int;

use isolang::Language;

#[repr(C)]
pub struct sb_stemmer {
stemmer: Stemmer,
stem: String,
// Whenever `stem` is derived, `out_length` is equal to `stem.len()`. But in some cases (e.g.
// non-UTF8), `stem` cannot be derived, in which case `out_length` captures the length of the
// input.
out_length: i32,
}

#[allow(non_camel_case_types)]
pub type sb_symbol = c_char;

include!("generated_language_list.rs");

#[no_mangle]
pub extern "C" fn sb_stemmer_list() -> *const *const c_char {
return ALGORITHM_NAMES_CSTR.as_ptr();
}

#[no_mangle]
pub extern "C" fn sb_stemmer_new(
algorithm: *const c_char,
_charenc: *const c_char,
) -> *mut sb_stemmer {
assert!(!algorithm.is_null());
let language_c_str: &CStr = unsafe { CStr::from_ptr(algorithm) };
let language_slice: &str = language_c_str.to_str().unwrap();
let language_name: Option<&str> = ALGORITHM_NAMES
.iter()
.position(|&name| name == language_slice)
.and_then(|i| Some(ALGORITHM_NAMES[i]))
.or_else(|| {
Language::from_639_1(language_slice)
.or_else(|| Language::from_639_3(language_slice))
.and_then(|language| {
ALGORITHM_NAMES
.iter()
.position(|&el| el == language.to_name().to_string().to_lowercase())
.and_then(|i| Some(ALGORITHM_NAMES[i]))
})
});

(match language_name {
Some(language_name) => Box::into_raw(Box::new(sb_stemmer {
stemmer: Stemmer::create(language_name.to_string()),
stem: String::new(),
out_length: 0,
})),
None => std::ptr::null(),
}) as *mut sb_stemmer
}

#[no_mangle]
pub extern "C" fn sb_stemmer_delete(stemmer: *mut sb_stemmer) {
if !stemmer.is_null() {
drop(unsafe { Box::from_raw(stemmer) });
}
}

/// Extract stem of a word.
///
/// * `stemmer` - pointer to a stemmer.
/// * `word` - pointer to the word.
/// * `size` - bytes length of the word to stem.
///
/// SAFETY: stemmer points to an initialized sb_stemmer, word points to a char
/// buffer, callers need to pass a size that is valid in their address space.
#[no_mangle]
pub unsafe extern "C" fn sb_stemmer_stem(
stemmer: *mut sb_stemmer,
word: *const sb_symbol,
size: c_int,
) -> *const sb_symbol {
assert!(!stemmer.is_null());
assert!(!word.is_null());
assert!(size >= 0);

let stemmer = unsafe { &mut *stemmer };

let word_slice: &[u8] = unsafe { std::slice::from_raw_parts(word as *const u8, size as usize) };
let word_slice: &str = match std::str::from_utf8(word_slice) {
Ok(word) => word,
Err(_) => {
stemmer.out_length = size;
return word;
}
};

let stem_cow = stemmer.stemmer.stem(word_slice);
match stem_cow {
std::borrow::Cow::Borrowed(b) => {
stemmer.stem.clear();
stemmer.stem.push_str(b);
}
std::borrow::Cow::Owned(o) => {
stemmer.stem = o;
}
}
stemmer.out_length = stemmer.stem.len() as i32;
stemmer.stem.as_ptr() as *const sb_symbol
}

/// Returns the length of the stem of the last word.
///
/// * `stemmer` - pointer to a stemmer.
///
/// SAFETY: stemmer points to an initialized sb_stemmer.
#[no_mangle]
pub unsafe extern "C" fn sb_stemmer_length(stemmer: *mut sb_stemmer) -> c_int {
if stemmer.is_null() {
return -1;
}

return unsafe { (*stemmer).out_length };
}
Loading
Loading