1/*
2 Simple DirectMedia Layer
3 Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
4
5 This software is provided 'as-is', without any express or implied
6 warranty. In no event will the authors be held liable for any damages
7 arising from the use of this software.
8
9 Permission is granted to anyone to use this software for any purpose,
10 including commercial applications, and to alter it and redistribute it
11 freely, subject to the following restrictions:
12
13 1. The origin of this software must not be misrepresented; you must not
14 claim that you wrote the original software. If you use this software
15 in a product, an acknowledgment in the product documentation would be
16 appreciated but is not required.
17 2. Altered source versions must be plainly marked as such, and must not be
18 misrepresented as being the original software.
19 3. This notice may not be removed or altered from any source distribution.
20*/
21
22#include "SDL_internal.h"
23#include "SDL_sysasyncio.h"
24#include "SDL_asyncio_c.h"
25
26static const char *AsyncFileModeValid(const char *mode)
27{
28 static const struct { const char *valid; const char *with_binary; } mode_map[] = {
29 { "r", "rb" },
30 { "w", "wb" },
31 { "r+","r+b" },
32 { "w+", "w+b" }
33 };
34
35 for (int i = 0; i < SDL_arraysize(mode_map); i++) {
36 if (SDL_strcmp(mode, mode_map[i].valid) == 0) {
37 return mode_map[i].with_binary;
38 }
39 }
40 return NULL;
41}
42
43SDL_AsyncIO *SDL_AsyncIOFromFile(const char *file, const char *mode)
44{
45 if (!file) {
46 SDL_InvalidParamError("file");
47 return NULL;
48 } else if (!mode) {
49 SDL_InvalidParamError("mode");
50 return NULL;
51 }
52
53 const char *binary_mode = AsyncFileModeValid(mode);
54 if (!binary_mode) {
55 SDL_SetError("Unsupported file mode");
56 return NULL;
57 }
58
59 SDL_AsyncIO *asyncio = (SDL_AsyncIO *)SDL_calloc(1, sizeof(*asyncio));
60 if (!asyncio) {
61 return NULL;
62 }
63
64 asyncio->lock = SDL_CreateMutex();
65 if (!asyncio->lock) {
66 SDL_free(asyncio);
67 return NULL;
68 }
69
70 if (!SDL_SYS_AsyncIOFromFile(file, binary_mode, asyncio)) {
71 SDL_DestroyMutex(asyncio->lock);
72 SDL_free(asyncio);
73 return NULL;
74 }
75
76 return asyncio;
77}
78
79Sint64 SDL_GetAsyncIOSize(SDL_AsyncIO *asyncio)
80{
81 if (!asyncio) {
82 SDL_InvalidParamError("asyncio");
83 return -1;
84 }
85 return asyncio->iface.size(asyncio->userdata);
86}
87
88static bool RequestAsyncIO(bool reading, SDL_AsyncIO *asyncio, void *ptr, Uint64 offset, Uint64 size, SDL_AsyncIOQueue *queue, void *userdata)
89{
90 if (!asyncio) {
91 return SDL_InvalidParamError("asyncio");
92 } else if (!ptr) {
93 return SDL_InvalidParamError("ptr");
94 } else if (!queue) {
95 return SDL_InvalidParamError("queue");
96 }
97
98 SDL_AsyncIOTask *task = (SDL_AsyncIOTask *) SDL_calloc(1, sizeof (*task));
99 if (!task) {
100 return false;
101 }
102
103 task->asyncio = asyncio;
104 task->type = reading ? SDL_ASYNCIO_TASK_READ : SDL_ASYNCIO_TASK_WRITE;
105 task->offset = offset;
106 task->buffer = ptr;
107 task->requested_size = size;
108 task->app_userdata = userdata;
109 task->queue = queue;
110
111 SDL_LockMutex(asyncio->lock);
112 if (asyncio->closing) {
113 SDL_free(task);
114 SDL_UnlockMutex(asyncio->lock);
115 return SDL_SetError("SDL_AsyncIO is closing, can't start new tasks");
116 }
117 LINKED_LIST_PREPEND(task, asyncio->tasks, asyncio);
118 SDL_AddAtomicInt(&queue->tasks_inflight, 1);
119 SDL_UnlockMutex(asyncio->lock);
120
121 const bool queued = reading ? asyncio->iface.read(asyncio->userdata, task) : asyncio->iface.write(asyncio->userdata, task);
122 if (!queued) {
123 SDL_AddAtomicInt(&queue->tasks_inflight, -1);
124 SDL_LockMutex(asyncio->lock);
125 LINKED_LIST_UNLINK(task, asyncio);
126 SDL_UnlockMutex(asyncio->lock);
127 SDL_free(task);
128 task = NULL;
129 }
130
131 return (task != NULL);
132}
133
134bool SDL_ReadAsyncIO(SDL_AsyncIO *asyncio, void *ptr, Uint64 offset, Uint64 size, SDL_AsyncIOQueue *queue, void *userdata)
135{
136 return RequestAsyncIO(true, asyncio, ptr, offset, size, queue, userdata);
137}
138
139bool SDL_WriteAsyncIO(SDL_AsyncIO *asyncio, void *ptr, Uint64 offset, Uint64 size, SDL_AsyncIOQueue *queue, void *userdata)
140{
141 return RequestAsyncIO(false, asyncio, ptr, offset, size, queue, userdata);
142}
143
144bool SDL_CloseAsyncIO(SDL_AsyncIO *asyncio, bool flush, SDL_AsyncIOQueue *queue, void *userdata)
145{
146 if (!asyncio) {
147 return SDL_InvalidParamError("asyncio");
148 } else if (!queue) {
149 return SDL_InvalidParamError("queue");
150 }
151
152 SDL_LockMutex(asyncio->lock);
153 if (asyncio->closing) {
154 SDL_UnlockMutex(asyncio->lock);
155 return SDL_SetError("Already closing");
156 }
157
158 SDL_AsyncIOTask *task = (SDL_AsyncIOTask *) SDL_calloc(1, sizeof (*task));
159 if (task) {
160 task->asyncio = asyncio;
161 task->type = SDL_ASYNCIO_TASK_CLOSE;
162 task->app_userdata = userdata;
163 task->queue = queue;
164 task->flush = flush;
165
166 asyncio->closing = task;
167
168 if (LINKED_LIST_START(asyncio->tasks, asyncio) == NULL) { // no tasks? Queue the close task now.
169 LINKED_LIST_PREPEND(task, asyncio->tasks, asyncio);
170 SDL_AddAtomicInt(&queue->tasks_inflight, 1);
171 if (!asyncio->iface.close(asyncio->userdata, task)) {
172 // uhoh, maybe they can try again later...?
173 SDL_AddAtomicInt(&queue->tasks_inflight, -1);
174 LINKED_LIST_UNLINK(task, asyncio);
175 SDL_free(task);
176 task = asyncio->closing = NULL;
177 }
178 }
179 }
180
181 SDL_UnlockMutex(asyncio->lock);
182
183 return (task != NULL);
184}
185
186SDL_AsyncIOQueue *SDL_CreateAsyncIOQueue(void)
187{
188 SDL_AsyncIOQueue *queue = SDL_calloc(1, sizeof (*queue));
189 if (queue) {
190 SDL_SetAtomicInt(&queue->tasks_inflight, 0);
191 if (!SDL_SYS_CreateAsyncIOQueue(queue)) {
192 SDL_free(queue);
193 return NULL;
194 }
195 }
196 return queue;
197}
198
199static bool GetAsyncIOTaskOutcome(SDL_AsyncIOTask *task, SDL_AsyncIOOutcome *outcome)
200{
201 if (!task || !outcome) {
202 return false;
203 }
204
205 SDL_AsyncIO *asyncio = task->asyncio;
206
207 SDL_zerop(outcome);
208 outcome->asyncio = asyncio->oneshot ? NULL : asyncio;
209 outcome->result = task->result;
210 outcome->type = task->type;
211 outcome->buffer = task->buffer;
212 outcome->offset = task->offset;
213 outcome->bytes_requested = task->requested_size;
214 outcome->bytes_transferred = task->result_size;
215 outcome->userdata = task->app_userdata;
216
217 // Take the completed task out of the SDL_AsyncIO that created it.
218 SDL_LockMutex(asyncio->lock);
219 LINKED_LIST_UNLINK(task, asyncio);
220 // see if it's time to queue a pending close request (close requested and no other pending tasks)
221 SDL_AsyncIOTask *closing = asyncio->closing;
222 if (closing && (task != closing) && (LINKED_LIST_START(asyncio->tasks, asyncio) == NULL)) {
223 LINKED_LIST_PREPEND(closing, asyncio->tasks, asyncio);
224 SDL_AddAtomicInt(&closing->queue->tasks_inflight, 1);
225 const bool async_close_task_was_queued = asyncio->iface.close(asyncio->userdata, closing);
226 SDL_assert(async_close_task_was_queued); // !!! FIXME: if this fails to queue the task, we're leaking resources!
227 if (!async_close_task_was_queued) {
228 SDL_AddAtomicInt(&closing->queue->tasks_inflight, -1);
229 }
230 }
231 SDL_UnlockMutex(task->asyncio->lock);
232
233 // was this the result of a closing task? Finally destroy the asyncio.
234 bool retval = true;
235 if (closing && (task == closing)) {
236 if (asyncio->oneshot) {
237 retval = false; // don't send the close task results on to the app, just the read task for these.
238 }
239 asyncio->iface.destroy(asyncio->userdata);
240 SDL_DestroyMutex(asyncio->lock);
241 SDL_free(asyncio);
242 }
243
244 SDL_AddAtomicInt(&task->queue->tasks_inflight, -1);
245 SDL_free(task);
246
247 return retval;
248}
249
250bool SDL_GetAsyncIOResult(SDL_AsyncIOQueue *queue, SDL_AsyncIOOutcome *outcome)
251{
252 if (!queue || !outcome) {
253 return false;
254 }
255 return GetAsyncIOTaskOutcome(queue->iface.get_results(queue->userdata), outcome);
256}
257
258bool SDL_WaitAsyncIOResult(SDL_AsyncIOQueue *queue, SDL_AsyncIOOutcome *outcome, Sint32 timeoutMS)
259{
260 if (!queue || !outcome) {
261 return false;
262 }
263 return GetAsyncIOTaskOutcome(queue->iface.wait_results(queue->userdata, timeoutMS), outcome);
264}
265
266void SDL_SignalAsyncIOQueue(SDL_AsyncIOQueue *queue)
267{
268 if (queue) {
269 queue->iface.signal(queue->userdata);
270 }
271}
272
273void SDL_DestroyAsyncIOQueue(SDL_AsyncIOQueue *queue)
274{
275 if (queue) {
276 // block until any pending tasks complete.
277 while (SDL_GetAtomicInt(&queue->tasks_inflight) > 0) {
278 SDL_AsyncIOTask *task = queue->iface.wait_results(queue->userdata, -1);
279 if (task) {
280 if (task->asyncio->oneshot) {
281 SDL_free(task->buffer); // throw away the buffer from SDL_LoadFileAsync that will never be consumed/freed by app.
282 task->buffer = NULL;
283 }
284 SDL_AsyncIOOutcome outcome;
285 GetAsyncIOTaskOutcome(task, &outcome); // this frees the task, and does other upkeep.
286 }
287 }
288
289 queue->iface.destroy(queue->userdata);
290 SDL_free(queue);
291 }
292}
293
294void SDL_QuitAsyncIO(void)
295{
296 SDL_SYS_QuitAsyncIO();
297}
298
299bool SDL_LoadFileAsync(const char *file, SDL_AsyncIOQueue *queue, void *userdata)
300{
301 if (!file) {
302 return SDL_InvalidParamError("file");
303 } else if (!queue) {
304 return SDL_InvalidParamError("queue");
305 }
306
307 bool retval = false;
308 SDL_AsyncIO *asyncio = SDL_AsyncIOFromFile(file, "r");
309 if (asyncio) {
310 asyncio->oneshot = true;
311
312 void *ptr = NULL;
313 const Sint64 flen = SDL_GetAsyncIOSize(asyncio);
314 if (flen >= 0) {
315 // !!! FIXME: check if flen > address space, since it'll truncate and we'll just end up with an incomplete buffer or a crash.
316 ptr = SDL_malloc((size_t) (flen + 1)); // over-allocate by one so we can add a null-terminator.
317 if (ptr) {
318 retval = SDL_ReadAsyncIO(asyncio, ptr, 0, (Uint64) flen, queue, userdata);
319 if (!retval) {
320 SDL_free(ptr);
321 }
322 }
323 }
324
325 SDL_CloseAsyncIO(asyncio, false, queue, userdata); // if this fails, we'll have a resource leak, but this would already be a dramatic system failure.
326 }
327
328 return retval;
329}
330
331