Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new version of modor_jobs #291

Merged
merged 4 commits into from
Apr 6, 2024
Merged
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
7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ rust-version = "1.75.0"
approx = "0.5"
android-activity = { version = "0.5", features = ["native-activity"] }
android_logger = "0.13"
async-std = "1.12"
cargo-run-wasm = "0.3"
console_error_panic_hook = "0.1"
console_log = "1.0"
darling = "0.20"
fs_extra = "1.2"
futures = "0.3"
fxhash = "0.2"
instant = "0.1"
log = "0.4"
Expand All @@ -27,13 +29,18 @@ pretty_env_logger = "0.5"
proc-macro-crate = "3.0"
proc-macro2 = "1.0"
rapier2d = "0.18"
reqwest = "0.12"
quote = "1.0"
syn = { version = "2.0", features = ["full"] }
wasm-bindgen-futures = "0.4"
wasm-bindgen-test = "0.3"
web-sys = { version = "0.3", features = ["Location"] }

modor = { version = "0.1.0", path = "crates/modor" }
modor_derive = { version = "0.1.0", path = "crates/modor_derive" }
modor_input = { version = "0.1.0", path = "crates/modor_input" }
modor_internal = { version = "0.1.0", path = "crates/modor_internal" }
modor_jobs = { version = "0.1.0", path = "crates/modor_jobs" }
modor_math = { version = "0.1.0", path = "crates/modor_math" }
modor_physics = { version = "0.1.0", path = "crates/modor_physics" }

Expand Down
1 change: 1 addition & 0 deletions PUBLISHED-CRATES
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ modor
modor_math
modor_physics
modor_input
modor_jobs
29 changes: 29 additions & 0 deletions crates/modor_jobs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[package]
name = "modor_jobs"
description = "Jobs module of Modor game engine"
readme = "./README.md"
keywords = ["modor", "job", "task", "async", "game"]
categories = ["game-engines"]
exclude = [".github", "README.md", "assets"]
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true

[dependencies]
futures.workspace = true
log.workspace = true
modor.workspace = true

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
async-std.workspace = true

[target.'cfg(target_arch = "wasm32")'.dependencies]
reqwest.workspace = true
wasm-bindgen-futures.workspace = true
web-sys.workspace = true

[lints]
workspace = true
3 changes: 3 additions & 0 deletions crates/modor_jobs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# modor_jobs

Jobs crate of [Modor](https://github.com/modor-engine/modor).
Empty file.
1 change: 1 addition & 0 deletions crates/modor_jobs/assets/test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
File content
137 changes: 137 additions & 0 deletions crates/modor_jobs/src/asset_loading_job.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
use crate::{platform, Job, VariableSend};
use std::any::Any;
use std::error::Error;
use std::fmt::{Debug, Display, Formatter};
use std::future::Future;

/// Name of the asset folder taken into account in the folder `CARGO_MANIFEST_DIR`.
pub const ASSET_FOLDER_NAME: &str = "assets";

/// An asynchronous job to retrieve an asset file.
///
/// # Example
///
/// ```rust
/// # use std::path::*;
/// # use modor::*;
/// # use modor_jobs::*;
/// #
/// struct AssetMetadata {
/// job: AssetLoadingJob<usize>,
/// size: Result<usize, AssetMetadataError>
/// }
///
/// impl AssetMetadata {
/// fn new(path: impl AsRef<str>) -> Self {
/// Self {
/// job: AssetLoadingJob::new(path, |b| async move { b.len() }),
/// size: Err(AssetMetadataError::NotReadYet),
/// }
/// }
///
/// fn size(&self) -> Result<usize, AssetMetadataError> {
/// self.size
/// }
///
/// fn poll(&mut self) {
/// match self.job.try_poll() {
/// Ok(Some(result)) => self.size = Ok(result),
/// Ok(None) => (),
/// Err(_) => self.size = Err(AssetMetadataError::LoadingError),
/// }
/// }
/// }
///
/// #[derive(Clone, Copy)]
/// enum AssetMetadataError {
/// NotReadYet,
/// LoadingError
/// }
/// ```
#[derive(Debug)]
pub struct AssetLoadingJob<T> {
/// Actual job instance that can be used to retrieve the job result.
inner: Job<Result<T, AssetLoadingError>>,
}

impl<T> AssetLoadingJob<T>
where
T: Any + VariableSend + Debug,
{
/// Creates a new job to retrieve asset located at `path`, and apply `f` on the bytes of the
/// file.
///
/// # Platform-specific
///
/// - Web: HTTP GET call is performed to retrieve the file from URL
/// `{current_browser_url}/assets/{path}`.
/// - Android: the file is retrieved using the Android
/// [`AssetManager`](https://developer.android.com/reference/android/content/res/AssetManager).
/// - Other: if `CARGO_MANIFEST_DIR` environment variable is set (this is the case if the
/// application is run using a `cargo` command), then the file is retrieved from path
/// `{CARGO_MANIFEST_DIR}/assets/{path}`. Else, the file path is
/// `{executable_folder_path}/assets/{path}`.
pub fn new<F>(path: impl AsRef<str>, f: impl FnOnce(Vec<u8>) -> F + VariableSend + Any) -> Self
where
F: Future<Output = T> + VariableSend,
{
let asset_path = path.as_ref().to_string();
Self {
inner: Job::<Result<T, AssetLoadingError>>::new(async move {
match platform::load_asset(asset_path).await {
Ok(b) => Ok(f(b).await),
Err(e) => Err(e),
}
}),
}
}

/// Try polling the job result.
///
/// `None` is returned if the result is not yet available or has already been retrieved.
///
/// # Errors
///
/// An error is returned if the asset has not been successfully loaded.
pub fn try_poll(&mut self) -> Result<Option<T>, AssetLoadingError> {
self.inner
.try_poll()
.expect("internal error: asset loading job has failed")
.map_or(Ok(None), |result| result.map(|r| Some(r)))
}
}

/// An error occurring during an asset loading job.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum AssetLoadingError {
/// The provided asset path contains unsupported characters.
InvalidAssetPath,
/// DOM `Window` object has not been found, can only occurs for web platform.
NotFoundDomWindow,
/// `location.href` property cannot be retrieved, can only occurs for web platform.
InvalidLocationHref(String),
/// I/O error occurred while retrieving the resource.
IoError(String),
/// App has not been correctly initialized (e.g. [`modor::main`] is not used).
InvalidAppInit,
}

// coverage: off (not necessary to test Display impl)
impl Display for AssetLoadingError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidAssetPath => write!(f, "invalid asset path"),
Self::NotFoundDomWindow => write!(f, "DOM window not found"),
Self::InvalidLocationHref(m) => write!(f, "invalid location.ref property: {m}"),
Self::IoError(m) => write!(f, "IO error: {m}"),
Self::InvalidAppInit => write!(
f,
"App incorrectly initialized (maybe modor::modor_main has not been used ?)"
),
}
}
}
// coverage: on

impl Error for AssetLoadingError {}
142 changes: 142 additions & 0 deletions crates/modor_jobs/src/job.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
use crate::platform;
use crate::platform::{JobFutureJoinHandle, VariableSend};
use futures::channel::oneshot;
use futures::channel::oneshot::{Receiver, Sender};
use std::any;
use std::any::Any;
use std::error::Error;
use std::fmt::{Debug, Display, Formatter};
use std::future::Future;

/// An asynchronous job.
///
/// # Example
///
/// ```rust
/// # use std::path::{Path, PathBuf};
/// # use modor::*;
/// # use modor_jobs::*;
/// #
/// struct FileReader {
/// job: Job<Vec<u8>>,
/// bytes: Result<Vec<u8>, FileReaderError>,
/// }
///
/// impl FileReader {
/// fn new(path: impl Into<PathBuf>) -> Self {
/// let path = path.into();
/// Self {
/// job: Job::new(async {
/// async_std::fs::read(path)
/// .await
/// .expect("cannot read file")
/// }),
/// bytes: Err(FileReaderError::NotReadYet),
/// }
/// }
///
/// fn bytes(&self) -> Result<&[u8], &FileReaderError> {
/// self.bytes.as_ref().map(Vec::as_slice)
/// }
///
/// fn poll(&mut self) {
/// match self.job.try_poll() {
/// Ok(Some(result)) => self.bytes = Ok(result),
/// Ok(None) => (),
/// Err(_) => self.bytes = Err(FileReaderError::IoError),
/// }
/// }
/// }
///
/// enum FileReaderError {
/// NotReadYet,
/// IoError,
/// }
/// ```
#[derive(Debug)]
pub struct Job<T> {
receiver: Option<Receiver<T>>,
_join: JobFutureJoinHandle<()>,
}

impl<T> Job<T>
where
T: Any + VariableSend + Debug,
{
/// Creates a new job to run a `future`.
///
/// # Panics
///
/// The future will panic if the [`Job`](Job) is dropped before the future has finished.
pub fn new(future: impl JobFuture<T>) -> Self {
let (sender, receiver) = oneshot::channel();
let job = Self::job_future(future, sender);
let join = platform::spawn_future(job);
debug!(
"job producing value of type `{}` has started", // no-coverage
any::type_name::<T>() // no-coverage
);
Self {
receiver: Some(receiver),
_join: join,
}
}

#[allow(clippy::future_not_send)]
async fn job_future(future: impl JobFuture<T>, sender: Sender<T>) {
sender
.send(future.await)
.expect("job dropped before future finishes");
}

/// Try polling the job result.
///
/// `None` is returned if the result is not yet available or has already been retrieved.
///
/// # Errors
///
/// An error is returned if the future run by a [`Job`](Job) has panicked.
pub fn try_poll(&mut self) -> Result<Option<T>, JobPanickedError> {
if let Some(receiver) = &mut self.receiver {
let result = receiver.try_recv().map_err(|_| JobPanickedError);
if let Ok(Some(_)) | Err(_) = &result {
self.receiver = None;
debug!(
"job producing value of type `{}` has finished", // no-coverage
any::type_name::<T>() // no-coverage
);
} else {
trace!(
"job producing value of type `{}` still in progress", // no-coverage
any::type_name::<T>() // no-coverage
);
}
result
} else {
debug!(
"job result of type `{}` already retrieved", // no-coverage
any::type_name::<T>() // no-coverage
);
Ok(None)
}
}
}

/// An error occurring when the future run by a [`Job`](Job) panics.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct JobPanickedError;

// coverage: off (not necessary to test Display impl)
impl Display for JobPanickedError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "job has panicked")
}
}
// coverage: on

impl Error for JobPanickedError {}

/// A trait implemented for any future runnable by a job that produces a value of type `T`.
pub trait JobFuture<T>: Future<Output = T> + VariableSend + Any {}

impl<F, T> JobFuture<T> for F where F: Future<Output = T> + VariableSend + Any {}
21 changes: 21 additions & 0 deletions crates/modor_jobs/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//! Jobs module of Modor.
//!
//! # Getting started
//!
//! You need to include these dependencies in your `Cargo.toml` file:
//! ```toml
//! modor_jobs = "0.1"
//! ```
//!
//! You can then use the components provided by this crate to start asynchronous jobs.

#[macro_use]
extern crate log;

mod asset_loading_job;
mod job;
mod platform;

pub use asset_loading_job::*;
pub use job::*;
pub use platform::*;
Loading
Loading