Skip to content

Commit 0fd6b1b

Browse files
committed
improve ffi str allocation
1 parent d1b5f5d commit 0fd6b1b

File tree

10 files changed

+47
-70
lines changed

10 files changed

+47
-70
lines changed

Cargo.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/fff-c/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ mimalloc.workspace = true
1313
once_cell.workspace = true
1414
tracing.workspace = true
1515
git2.workspace = true
16-
dirs.workspace = true
1716

1817
fff-core = { path = "../fff-core" }
1918
fff-query-parser = { path = "../fff-query-parser" }

crates/fff-c/src/ffi_types.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//! These types use #[repr(C)] for C ABI compatibility and implement
44
//! serde traits for JSON serialization.
55
6-
use std::ffi::{CString, c_char};
6+
use std::ffi::{c_char, CString};
77
use std::ptr;
88

99
use fff_core::git::format_git_status;
@@ -56,16 +56,13 @@ impl FffResult {
5656
pub struct InitOptions {
5757
/// Base directory to index (required)
5858
pub base_path: String,
59-
/// Path to frecency database (optional, defaults to ~/.fff/frecency.mdb)
59+
/// Path to frecency database (optional, omit to skip frecency initialization)
6060
pub frecency_db_path: Option<String>,
61-
/// Path to query history database (optional, defaults to ~/.fff/history.mdb)
61+
/// Path to query history database (optional, omit to skip query tracker initialization)
6262
pub history_db_path: Option<String>,
6363
/// Use unsafe no-lock mode for databases (optional, defaults to false)
6464
#[serde(default)]
6565
pub use_unsafe_no_lock: bool,
66-
/// Skip database initialization entirely (optional, defaults to false)
67-
#[serde(default)]
68-
pub skip_databases: bool,
6966
}
7067

7168
/// Search options (JSON-deserializable)

crates/fff-c/src/lib.rs

Lines changed: 35 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,17 @@ use mimalloc::MiMalloc;
2424
#[global_allocator]
2525
static GLOBAL: MiMalloc = MiMalloc;
2626

27-
/// Helper to convert C string to Rust string
28-
unsafe fn cstr_to_string(s: *const c_char) -> Option<String> {
27+
/// Helper to convert C string to Rust &str.
28+
///
29+
/// Returns `None` if the pointer is null or the string is not valid UTF-8.
30+
/// This is more efficient than `to_string_lossy()` as it returns a borrowed
31+
/// `&str` directly without `Cow` overhead, and avoids replacement character
32+
/// scanning since callers are expected to provide valid UTF-8.
33+
unsafe fn cstr_to_str<'a>(s: *const c_char) -> Option<&'a str> {
2934
if s.is_null() {
3035
None
3136
} else {
32-
unsafe { CStr::from_ptr(s).to_str().ok().map(|s| s.to_string()) }
37+
unsafe { CStr::from_ptr(s).to_str().ok() }
3338
}
3439
}
3540

@@ -39,42 +44,23 @@ unsafe fn cstr_to_string(s: *const c_char) -> Option<String> {
3944
/// `opts_json` must be a valid null-terminated UTF-8 string
4045
#[unsafe(no_mangle)]
4146
pub unsafe extern "C" fn fff_init(opts_json: *const c_char) -> *mut FffResult {
42-
let opts_str = match unsafe { cstr_to_string(opts_json) } {
47+
let opts_str = match unsafe { cstr_to_str(opts_json) } {
4348
Some(s) => s,
4449
None => return FffResult::err("Options JSON is null or invalid UTF-8"),
4550
};
4651

47-
let opts: InitOptions = match serde_json::from_str(&opts_str) {
52+
let opts: InitOptions = match serde_json::from_str(opts_str) {
4853
Ok(o) => o,
4954
Err(e) => return FffResult::err(&format!("Failed to parse options: {}", e)),
5055
};
5156

52-
// Initialize databases if not skipped
53-
if !opts.skip_databases {
54-
let frecency_path = opts.frecency_db_path.unwrap_or_else(|| {
55-
dirs::home_dir()
56-
.unwrap_or_else(|| PathBuf::from("."))
57-
.join(".fff")
58-
.join("frecency.mdb")
59-
.to_string_lossy()
60-
.to_string()
61-
});
62-
63-
let history_path = opts.history_db_path.unwrap_or_else(|| {
64-
dirs::home_dir()
65-
.unwrap_or_else(|| PathBuf::from("."))
66-
.join(".fff")
67-
.join("history.mdb")
68-
.to_string_lossy()
69-
.to_string()
70-
});
71-
57+
// Initialize frecency tracker if path is provided
58+
if let Some(frecency_path) = opts.frecency_db_path {
7259
// Ensure directory exists
7360
if let Some(parent) = PathBuf::from(&frecency_path).parent() {
7461
let _ = std::fs::create_dir_all(parent);
7562
}
7663

77-
// Initialize frecency tracker
7864
let mut frecency = match FRECENCY.write() {
7965
Ok(f) => f,
8066
Err(e) => return FffResult::err(&format!("Failed to acquire frecency lock: {}", e)),
@@ -85,8 +71,15 @@ pub unsafe extern "C" fn fff_init(opts_json: *const c_char) -> *mut FffResult {
8571
Err(e) => return FffResult::err(&format!("Failed to init frecency db: {}", e)),
8672
}
8773
drop(frecency);
74+
}
75+
76+
// Initialize query tracker if path is provided
77+
if let Some(history_path) = opts.history_db_path {
78+
// Ensure directory exists
79+
if let Some(parent) = PathBuf::from(&history_path).parent() {
80+
let _ = std::fs::create_dir_all(parent);
81+
}
8882

89-
// Initialize query tracker
9083
let mut query_tracker = match QUERY_TRACKER.write() {
9184
Ok(q) => q,
9285
Err(e) => {
@@ -159,16 +152,16 @@ pub unsafe extern "C" fn fff_search(
159152
query: *const c_char,
160153
opts_json: *const c_char,
161154
) -> *mut FffResult {
162-
let query_str = match unsafe { cstr_to_string(query) } {
155+
let query_str = match unsafe { cstr_to_str(query) } {
163156
Some(s) => s,
164157
None => return FffResult::err("Query is null or invalid UTF-8"),
165158
};
166159

167160
let opts: SearchOptions = if opts_json.is_null() {
168161
SearchOptions::default()
169162
} else {
170-
unsafe { cstr_to_string(opts_json) }
171-
.and_then(|s| serde_json::from_str(&s).ok())
163+
unsafe { cstr_to_str(opts_json) }
164+
.and_then(|s| serde_json::from_str(s).ok())
172165
.unwrap_or_default()
173166
};
174167

@@ -194,19 +187,19 @@ pub unsafe extern "C" fn fff_search(
194187

195188
query_tracker.as_ref().and_then(|tracker| {
196189
tracker
197-
.get_last_query_entry(&query_str, base_path, min_combo_count)
190+
.get_last_query_entry(query_str, base_path, min_combo_count)
198191
.ok()
199192
.flatten()
200193
})
201194
};
202195

203196
// Parse the query
204197
let parser = QueryParser::default();
205-
let parsed = parser.parse(&query_str);
198+
let parsed = parser.parse(query_str);
206199

207200
let results = FilePicker::fuzzy_search(
208201
picker.get_files(),
209-
&query_str,
202+
query_str,
210203
parsed,
211204
FuzzySearchOptions {
212205
max_threads: opts.max_threads.unwrap_or(0),
@@ -322,7 +315,7 @@ pub extern "C" fn fff_wait_for_scan(timeout_ms: u64) -> *mut FffResult {
322315
/// `new_path` must be a valid null-terminated UTF-8 string
323316
#[unsafe(no_mangle)]
324317
pub unsafe extern "C" fn fff_restart_index(new_path: *const c_char) -> *mut FffResult {
325-
let path_str = match unsafe { cstr_to_string(new_path) } {
318+
let path_str = match unsafe { cstr_to_str(new_path) } {
326319
Some(s) => s,
327320
None => return FffResult::err("Path is null or invalid UTF-8"),
328321
};
@@ -367,7 +360,7 @@ pub unsafe extern "C" fn fff_restart_index(new_path: *const c_char) -> *mut FffR
367360
/// `file_path` must be a valid null-terminated UTF-8 string
368361
#[unsafe(no_mangle)]
369362
pub unsafe extern "C" fn fff_track_access(file_path: *const c_char) -> *mut FffResult {
370-
let path_str = match unsafe { cstr_to_string(file_path) } {
363+
let path_str = match unsafe { cstr_to_str(file_path) } {
371364
Some(s) => s,
372365
None => return FffResult::err("File path is null or invalid UTF-8"),
373366
};
@@ -439,12 +432,12 @@ pub unsafe extern "C" fn fff_track_query(
439432
query: *const c_char,
440433
file_path: *const c_char,
441434
) -> *mut FffResult {
442-
let query_str = match unsafe { cstr_to_string(query) } {
435+
let query_str = match unsafe { cstr_to_str(query) } {
443436
Some(s) => s,
444437
None => return FffResult::err("Query is null or invalid UTF-8"),
445438
};
446439

447-
let path_str = match unsafe { cstr_to_string(file_path) } {
440+
let path_str = match unsafe { cstr_to_str(file_path) } {
448441
Some(s) => s,
449442
None => return FffResult::err("File path is null or invalid UTF-8"),
450443
};
@@ -471,7 +464,7 @@ pub unsafe extern "C" fn fff_track_query(
471464
};
472465

473466
if let Some(ref mut tracker) = *query_tracker
474-
&& let Err(e) = tracker.track_query_completion(&query_str, &project_path, &file_path)
467+
&& let Err(e) = tracker.track_query_completion(query_str, &project_path, &file_path)
475468
{
476469
return FffResult::err(&format!("Failed to track query: {}", e));
477470
}
@@ -519,7 +512,7 @@ pub extern "C" fn fff_get_historical_query(offset: u64) -> *mut FffResult {
519512
/// `test_path` can be null or a valid null-terminated UTF-8 string
520513
#[unsafe(no_mangle)]
521514
pub unsafe extern "C" fn fff_health_check(test_path: *const c_char) -> *mut FffResult {
522-
let test_path = unsafe { cstr_to_string(test_path) }
515+
let test_path = unsafe { cstr_to_str(test_path) }
523516
.filter(|s| !s.is_empty())
524517
.map(PathBuf::from)
525518
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
@@ -689,14 +682,13 @@ pub unsafe extern "C" fn fff_shorten_path(
689682
max_size: u64,
690683
strategy: *const c_char,
691684
) -> *mut FffResult {
692-
let path_str = match unsafe { cstr_to_string(path) } {
685+
let path_str = match unsafe { cstr_to_str(path) } {
693686
Some(s) => s,
694687
None => return FffResult::err("Path is null or invalid UTF-8"),
695688
};
696689

697-
let strategy_str =
698-
unsafe { cstr_to_string(strategy) }.unwrap_or_else(|| "middle_number".to_string());
699-
let strategy = fff_core::PathShortenStrategy::from_name(&strategy_str);
690+
let strategy_str = unsafe { cstr_to_str(strategy) }.unwrap_or("middle_number");
691+
let strategy = fff_core::PathShortenStrategy::from_name(strategy_str);
700692

701693
match fff_core::shorten_path(strategy, max_size as usize, &PathBuf::from(path_str)) {
702694
Ok(shortened) => {

packages/fff/README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,9 @@ Initialize the file finder.
5454
```typescript
5555
interface InitOptions {
5656
basePath: string; // Directory to index (required)
57-
frecencyDbPath?: string; // Custom frecency DB path
58-
historyDbPath?: string; // Custom history DB path
57+
frecencyDbPath?: string; // Frecency DB path (omit to skip frecency)
58+
historyDbPath?: string; // History DB path (omit to skip query tracking)
5959
useUnsafeNoLock?: boolean; // Faster but less safe DB mode
60-
skipDatabases?: boolean; // Skip frecency/history (simpler mode)
6160
}
6261

6362
const result = FileFinder.init({ basePath: "/my/project" });
@@ -126,7 +125,7 @@ if (health.ok) {
126125
- `FileFinder.isScanning()` - Check scan status
127126
- `FileFinder.getScanProgress()` - Get scan progress
128127
- `FileFinder.waitForScan(timeoutMs)` - Wait for scan
129-
- `FileFinder.restartIndex(newPath)` - Change indexed directory
128+
- `FileFinder.reindex(newPath)` - Change indexed directory
130129
- `FileFinder.refreshGitStatus()` - Refresh git cache
131130
- `FileFinder.getHistoricalQuery(offset)` - Get past queries
132131
- `FileFinder.shortenPath(path, maxSize, strategy)` - Shorten paths

packages/fff/examples/search.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ async function main() {
8888
console.log(`${DIM}Initializing index for: ${targetDir}${RESET}`);
8989
const initResult = FileFinder.init({
9090
basePath: targetDir,
91-
skipDatabases: true, // Skip frecency DB for demo simplicity
9291
});
9392

9493
if (!initResult.ok) {

packages/fff/src/finder.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ export class FileFinder {
8585
* historyDbPath: "/custom/history.mdb",
8686
* });
8787
*
88-
* // Minimal mode (no databases)
89-
* FileFinder.init({ basePath: "/path/to/project", skipDatabases: true });
88+
* // Minimal mode (no databases - just omit db paths)
89+
* FileFinder.init({ basePath: "/path/to/project" });
9090
* ```
9191
*/
9292
static init(options: InitOptions): Result<void> {
@@ -214,7 +214,7 @@ export class FileFinder {
214214
*
215215
* @param newPath - New directory path to index
216216
*/
217-
static restartIndex(newPath: string): Result<void> {
217+
static reindex(newPath: string): Result<void> {
218218
if (!this.initialized) {
219219
return err("FileFinder not initialized. Call FileFinder.init() first.");
220220
}
@@ -297,7 +297,7 @@ export class FileFinder {
297297
static shortenPath(
298298
path: string,
299299
maxSize: number,
300-
strategy: "middle_number" | "beginning" | "end" = "middle_number"
300+
strategy: "middle_number" | "beginning" | "end" = "middle_number",
301301
): Result<string> {
302302
return ffiShortenPath(path, maxSize, strategy);
303303
}

packages/fff/src/index.test.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ describe("FileFinder - Full Lifecycle", () => {
8888
test("init succeeds with valid path", () => {
8989
const result = FileFinder.init({
9090
basePath: testDir,
91-
skipDatabases: true,
9291
});
9392

9493
expect(result.ok).toBe(true);
@@ -184,7 +183,6 @@ describe("FileFinder - Full Lifecycle", () => {
184183

185184
const result = FileFinder.init({
186185
basePath: testDir,
187-
skipDatabases: true,
188186
});
189187
expect(result.ok).toBe(true);
190188
expect(FileFinder.isInitialized()).toBe(true);
@@ -232,7 +230,6 @@ describe("FileFinder - Error Handling", () => {
232230
test("init fails with invalid path", () => {
233231
const result = FileFinder.init({
234232
basePath: "/nonexistent/path/that/does/not/exist",
235-
skipDatabases: true,
236233
});
237234

238235
expect(result.ok).toBe(false);

packages/fff/src/types.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,12 @@ export function err<T>(error: string): Result<T> {
2525
export interface InitOptions {
2626
/** Base directory to index (required) */
2727
basePath: string;
28-
/** Path to frecency database (optional, defaults to ~/.fff/frecency.mdb) */
28+
/** Path to frecency database (optional, omit to skip frecency initialization) */
2929
frecencyDbPath?: string;
30-
/** Path to query history database (optional, defaults to ~/.fff/history.mdb) */
30+
/** Path to query history database (optional, omit to skip query tracker initialization) */
3131
historyDbPath?: string;
3232
/** Use unsafe no-lock mode for databases (optional, defaults to false) */
3333
useUnsafeNoLock?: boolean;
34-
/** Skip database initialization entirely (optional, defaults to false) */
35-
skipDatabases?: boolean;
3634
}
3735

3836
/**
@@ -212,7 +210,6 @@ export interface InitOptionsInternal {
212210
frecency_db_path?: string;
213211
history_db_path?: string;
214212
use_unsafe_no_lock: boolean;
215-
skip_databases: boolean;
216213
}
217214

218215
/**
@@ -238,7 +235,6 @@ export function toInternalInitOptions(opts: InitOptions): InitOptionsInternal {
238235
frecency_db_path: opts.frecencyDbPath,
239236
history_db_path: opts.historyDbPath,
240237
use_unsafe_no_lock: opts.useUnsafeNoLock ?? false,
241-
skip_databases: opts.skipDatabases ?? false,
242238
};
243239
}
244240

packages/fff/test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ async function main() {
3939

4040
const initResult = FileFinder.init({
4141
basePath: testDir,
42-
skipDatabases: true, // Skip DB for simple test
4342
});
4443

4544
if (!initResult.ok) {

0 commit comments

Comments
 (0)