Skip to content

Commit 598fc19

Browse files
authored
Fix: Download with progress callback fails with 'Operation was cancel… (#693)
## Fix: Download with progress callback fails with "Operation was cancelled by user" ### Problem Calling `model->Download()` with a progress callback causes the native core to abort with: ``` [Foundry][Error] Error downloading model [qwen3.5-2b-generic-gpu]: Operation was cancelled by user ``` The same call without a callback works correctly. ### Root Cause The C++ SDK declared `NativeCallbackFn` as returning `void`, while the native core DLL (C#) expects the callback to return `int` (`0` = continue, `1` = cancel). Since the `void` callback never sets the return register, the C# marshalling reads an indeterminate value (garbage), interprets it as non-zero (cancel), and aborts the download. All other SDKs correctly define the callback as returning `int`: | SDK | Callback return type | Correct? | |-----|---------------------|----------| | **C++ (before fix)** | **`void`** | **No** | | C# | `int` | Yes | | JavaScript | `int32_t` | Yes | | Python | `c_int` | Yes | | Rust | `i32` | Yes | ### Fix Changed `NativeCallbackFn` and `UserCallbackFn` from `void(*)()` to `int(*)()` and updated all callback lambdas to return `0` (continue). ### Files Changed - **`sdk/cpp/src/foundry_local_internal_core.h`** — `NativeCallbackFn` return type `void` → `int` - **`sdk/cpp/src/flcore_native.h`** — `UserCallbackFn` return type `void` → `int` - **`sdk/cpp/src/model.cpp`** — download progress callback lambda returns `0` - **`sdk/cpp/src/core_helpers.h`** — streaming callback lambda returns `0` - **`sdk/cpp/test/model_variant_test.cpp`** — added regression test `Download_WithCallback_ReturnsZeroToContinue` ### Testing All 163 unit tests pass, including a new regression test that invokes the callback and asserts the return value is `0`.
1 parent b379679 commit 598fc19

5 files changed

Lines changed: 28 additions & 8 deletions

File tree

sdk/cpp/src/core_helpers.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,13 @@ namespace foundry_local::detail {
6060
std::exception_ptr exception;
6161
} state{&onChunk, nullptr};
6262

63-
auto nativeCallback = [](void* data, int32_t len, void* user) {
63+
auto nativeCallback = [](void* data, int32_t len, void* user) -> int {
6464
if (!data || len <= 0)
65-
return;
65+
return 0;
6666

6767
auto* st = static_cast<State*>(user);
6868
if (st->exception)
69-
return;
69+
return 0;
7070

7171
try {
7272
std::string chunk(static_cast<const char*>(data), static_cast<size_t>(len));
@@ -75,6 +75,7 @@ namespace foundry_local::detail {
7575
catch (...) {
7676
st->exception = std::current_exception();
7777
}
78+
return 0;
7879
};
7980

8081
auto response = core->call(command, logger, &payload, +nativeCallback, &state);

sdk/cpp/src/flcore_native.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ extern "C"
2323
int32_t ErrorLength;
2424
};
2525

26-
// Callback signature: void(*)(void* data, int length, void* userData)
27-
using UserCallbackFn = void(__cdecl*)(void*, int32_t, void*);
26+
// Callback signature: int(*)(void* data, int length, void* userData) — returns 0 to continue, 1 to cancel
27+
using UserCallbackFn = int(__cdecl*)(void*, int32_t, void*);
2828

2929
struct StreamingRequestBuffer {
3030
const void* Command;

sdk/cpp/src/foundry_local_internal_core.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace foundry_local {
1212

1313
/// Native callback signature used by the core DLL interop.
1414
/// Parameters: (data, dataLength, userData).
15-
using NativeCallbackFn = void (*)(void*, int32_t, void*);
15+
using NativeCallbackFn = int (*)(void*, int32_t, void*);
1616

1717
/// Value returned by IFoundryLocalCore::call().
1818
/// On success, `data` contains the response payload and `error` is empty.

sdk/cpp/src/model.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,9 @@ namespace foundry_local {
7979
ILogger* logger;
8080
} state{&onProgress, logger_};
8181

82-
auto nativeCallback = [](void* data, int32_t len, void* user) {
82+
auto nativeCallback = [](void* data, int32_t len, void* user) -> int {
8383
if (!data || len <= 0)
84-
return;
84+
return 0;
8585
auto* st = static_cast<ProgressState*>(user);
8686
std::string perc(static_cast<char*>(data), static_cast<size_t>(len));
8787
try {
@@ -91,6 +91,7 @@ namespace foundry_local {
9191
catch (...) {
9292
st->logger->Log(LogLevel::Warning, "Failed to parse download progress: " + perc);
9393
}
94+
return 0;
9495
};
9596

9697
auto response = CallWithJsonAndCallback(core_, "download_model", MakeModelParams(info_.name), *logger_,

sdk/cpp/test/model_variant_test.cpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,24 @@ TEST_F(ModelVariantTest, Download_WithCallback) {
128128
EXPECT_NEAR(50.0f, lastProgress, 0.01f);
129129
}
130130

131+
// Regression test: the native core DLL expects the callback to return int (0 = continue, 1 = cancel).
132+
// Previously NativeCallbackFn returned void, causing the core to read garbage as a cancel signal.
133+
TEST_F(ModelVariantTest, Download_WithCallback_ReturnsZeroToContinue) {
134+
core_.OnCall("get_cached_models", R"([])");
135+
core_.OnCall("download_model",
136+
[](std::string_view, const std::string*, NativeCallbackFn callback, void* userData) -> std::string {
137+
if (callback && userData) {
138+
std::string progress = "50";
139+
int result = callback(progress.data(), static_cast<int32_t>(progress.size()), userData);
140+
EXPECT_EQ(0, result) << "Callback should return 0 (continue), not " << result;
141+
}
142+
return "";
143+
});
144+
145+
auto variant = MakeVariant("test-model");
146+
variant.Download([&](float) {});
147+
}
148+
131149
TEST_F(ModelVariantTest, RemoveFromCache_CallsCore) {
132150
core_.OnCall("remove_cached_model", "");
133151
auto variant = MakeVariant("test-model");

0 commit comments

Comments
 (0)