diff --git a/Android/README.md b/Android/README.md index ed9145e..23c5d88 100644 --- a/Android/README.md +++ b/Android/README.md @@ -1,5 +1,5 @@ - + # Android @@ -20,7 +20,7 @@ Chaque module `game-*` est une application Android indépendante ; il dépend du ## Toolchain de référence -Pour la baseline `0.1.0-0-pre.6` : +Pour la baseline `0.1.0-0-pre.8` : - Android Gradle Plugin `9.4.0` ; - Gradle `9.6.0` attendu par AGP 9.4 ; @@ -70,3 +70,9 @@ python3 ../scripts/build_android_rust.py snake ``` Le script extrait temporairement `libSDL3.so` de l'AAR uniquement pour fournir le chemin de linkage à `rustc`; l'AAR reste responsable du packaging SDL3 dans l'APK. + +## Bridge JNI et tactile + +À partir de `0.1.0-0-pre.8`, `SaseGameActivity` vérifie au démarrage la version du bridge Java/JNI chargé depuis la bibliothèque Rust. + +Les touch events standards restent traités via SDL3 et ne transitent pas par JNI. diff --git a/Android/common/src/main/java/com/sasedev/games/common/NativeBridge.java b/Android/common/src/main/java/com/sasedev/games/common/NativeBridge.java index ce5cf7e..08c5cf3 100644 --- a/Android/common/src/main/java/com/sasedev/games/common/NativeBridge.java +++ b/Android/common/src/main/java/com/sasedev/games/common/NativeBridge.java @@ -1,19 +1,37 @@ // file: Android/common/src/main/java/com/sasedev/games/common/NativeBridge.java -// version: 1 +// version: 2 package com.sasedev.games.common; -/** Stable Java side of the future Java/JNI platform bridge. */ +/** Stable Java/JNI platform bridge contract shared by Android game applications. */ public final class NativeBridge { + private static final int CONTRACT_VERSION = 1; + private NativeBridge() { } /** - * Returns the bridge contract version. + * Returns the Java-side bridge contract version. * * @return bridge contract version */ public static int contractVersion() { - return 1; + return CONTRACT_VERSION; + } + + /** + * Returns the Rust-side bridge contract version. + * + * @return native bridge contract version + */ + public static native int nativeContractVersion(); + + /** Verifies that the loaded native bridge matches the Java contract. */ + public static void requireCompatibleContract() { + final int nativeVersion = nativeContractVersion(); + if (nativeVersion != CONTRACT_VERSION) { + throw new IllegalStateException( + "Native bridge contract mismatch: java=" + CONTRACT_VERSION + ", native=" + nativeVersion); + } } } diff --git a/Android/common/src/main/java/com/sasedev/games/common/SaseGameActivity.java b/Android/common/src/main/java/com/sasedev/games/common/SaseGameActivity.java index f386733..09e2f51 100644 --- a/Android/common/src/main/java/com/sasedev/games/common/SaseGameActivity.java +++ b/Android/common/src/main/java/com/sasedev/games/common/SaseGameActivity.java @@ -1,8 +1,10 @@ // file: Android/common/src/main/java/com/sasedev/games/common/SaseGameActivity.java -// version: 3 +// version: 4 package com.sasedev.games.common; +import android.os.Bundle; + /** * Common Android activity boundary for games.sasedev applications. * @@ -10,6 +12,13 @@ package com.sasedev.games.common; */ public abstract class SaseGameActivity extends org.libsdl.app.SDLActivity { + /** {@inheritDoc} */ + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + NativeBridge.requireCompatibleContract(); + } + /** {@inheritDoc} */ @Override protected final String[] getLibraries() { diff --git a/Android/game-reflex-poc/build.gradle b/Android/game-reflex-poc/build.gradle index 5de0e6e..6548066 100644 --- a/Android/game-reflex-poc/build.gradle +++ b/Android/game-reflex-poc/build.gradle @@ -1,5 +1,5 @@ // file: Android/game-reflex-poc/build.gradle -// version: 5 +// version: 6 plugins { id 'com.android.application' @@ -16,7 +16,7 @@ android { minSdk 21 targetSdk 36 versionCode 1 - versionName '0.1.0-0-pre.7.fix.1' + versionName '0.1.0-0-pre.8' } compileOptions { diff --git a/Android/game-snake-poc/build.gradle b/Android/game-snake-poc/build.gradle index 43d9832..3d1fcb6 100644 --- a/Android/game-snake-poc/build.gradle +++ b/Android/game-snake-poc/build.gradle @@ -1,5 +1,5 @@ // file: Android/game-snake-poc/build.gradle -// version: 5 +// version: 6 plugins { id 'com.android.application' @@ -16,7 +16,7 @@ android { minSdk 21 targetSdk 36 versionCode 1 - versionName '0.1.0-0-pre.7.fix.1' + versionName '0.1.0-0-pre.8' } compileOptions { diff --git a/Cargo.toml b/Cargo.toml index b344783..4081c6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ # file: Cargo.toml -# version: 15 +# version: 16 [workspace] resolver = "3" @@ -16,7 +16,7 @@ members = [ ] [workspace.package] -version = "0.1.0-0-pre.7.fix.1" +version = "0.1.0-0-pre.8" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/games" diff --git a/crates/apps/game-android-entrypoint/src/lib.rs b/crates/apps/game-android-entrypoint/src/lib.rs index ed827ed..6733df2 100644 --- a/crates/apps/game-android-entrypoint/src/lib.rs +++ b/crates/apps/game-android-entrypoint/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/apps/game-android-entrypoint/src/lib.rs -// version: 1 +// version: 2 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -19,6 +19,11 @@ extern "C" fn sdl_main(_argc: core::ffi::c_int, _argv: *mut *mut core::ffi::c_ch return run_selected_game(); } +#[unsafe(export_name = "Java_com_sasedev_games_common_NativeBridge_nativeContractVersion")] +extern "C" fn native_contract_version(_environment: *mut core::ffi::c_void, _class: *mut core::ffi::c_void) -> core::ffi::c_int { + return 1; +} + #[cfg(feature = "reflex")] fn run_selected_game() -> core::ffi::c_int { let runtime = engine_v1_sdl::SdlRuntime::new("Reflex POC", 405, 720, std::time::Duration::from_millis(16)); diff --git a/crates/apps/game-android-entrypoint/unit_tests/selection.rs b/crates/apps/game-android-entrypoint/unit_tests/selection.rs index b07f130..e8aab43 100644 --- a/crates/apps/game-android-entrypoint/unit_tests/selection.rs +++ b/crates/apps/game-android-entrypoint/unit_tests/selection.rs @@ -1,8 +1,13 @@ // file: crates/apps/game-android-entrypoint/unit_tests/selection.rs -// version: 1 +// version: 2 #[test] fn no_feature_entrypoint_has_failure_status() { #[cfg(not(any(feature = "reflex", feature = "snake")))] assert_eq!(crate::run_selected_game(), 2); } + +#[test] +fn native_bridge_contract_version_is_one() { + assert_eq!(crate::native_contract_version(core::ptr::null_mut(), core::ptr::null_mut()), 1); +} diff --git a/crates/engines/engine-v1-common/src/input.rs b/crates/engines/engine-v1-common/src/input.rs index 899b0b4..e504cbc 100644 --- a/crates/engines/engine-v1-common/src/input.rs +++ b/crates/engines/engine-v1-common/src/input.rs @@ -1,8 +1,8 @@ // file: crates/engines/engine-v1-common/src/input.rs -// version: 2 +// version: 3 /// Platform-independent snapshot of logical input actions for one update. -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct InputState { left: bool, right: bool, @@ -11,13 +11,23 @@ pub struct InputState { primary: bool, secondary: bool, pause: bool, + pointer: crate::PointerState, } impl InputState { /// Creates a snapshot with no active action. #[must_use] pub const fn none() -> Self { - return Self { left: false, right: false, up: false, down: false, primary: false, secondary: false, pause: false }; + return Self { + left: false, + right: false, + up: false, + down: false, + primary: false, + secondary: false, + pause: false, + pointer: crate::PointerState::inactive(), + }; } /// Returns a copy with the selected logical action set to the requested state. @@ -35,6 +45,13 @@ impl InputState { return self; } + /// Returns a copy carrying the selected normalized primary pointer state. + #[must_use] + pub const fn with_pointer(mut self, pointer: crate::PointerState) -> Self { + self.pointer = pointer; + return self; + } + /// Reports whether the selected logical action is active. #[must_use] pub const fn is_active(self, action: crate::GameAction) -> bool { @@ -48,6 +65,12 @@ impl InputState { crate::GameAction::Pause => self.pause, }; } + + /// Returns the primary pointer snapshot. + #[must_use] + pub const fn pointer(self) -> crate::PointerState { + return self.pointer; + } } #[cfg(test)] diff --git a/crates/engines/engine-v1-common/src/lib.rs b/crates/engines/engine-v1-common/src/lib.rs index 2bac289..1702fbc 100644 --- a/crates/engines/engine-v1-common/src/lib.rs +++ b/crates/engines/engine-v1-common/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/engines/engine-v1-common/src/lib.rs -// version: 2 +// version: 3 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -11,6 +11,7 @@ mod action; mod frame; mod game_loop; mod input; +mod pointer; /// Re-export of the canonical logical input action used by engine V1 games. pub use self::action::GameAction; @@ -22,3 +23,5 @@ pub use self::game_loop::EngineGame; pub use self::game_loop::FixedStepRunner; /// Re-export of the platform-independent logical input snapshot. pub use self::input::InputState; +/// Re-export of the normalized primary pointer snapshot. +pub use self::pointer::PointerState; diff --git a/crates/engines/engine-v1-common/src/pointer.rs b/crates/engines/engine-v1-common/src/pointer.rs new file mode 100644 index 0000000..aae7773 --- /dev/null +++ b/crates/engines/engine-v1-common/src/pointer.rs @@ -0,0 +1,52 @@ +// file: crates/engines/engine-v1-common/src/pointer.rs +// version: 1 + +/// Platform-independent normalized primary pointer state for one engine update. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct PointerState { + active: bool, + x: f32, + y: f32, +} + +impl PointerState { + /// Creates an inactive pointer at the normalized origin. + #[must_use] + pub const fn inactive() -> Self { + return Self { active: false, x: 0.0, y: 0.0 }; + } + + /// Creates a pointer state from normalized coordinates. + #[must_use] + pub fn normalized(active: bool, x: f32, y: f32) -> Self { + return Self { active, x: x.clamp(0.0, 1.0), y: y.clamp(0.0, 1.0) }; + } + + /// Reports whether the pointer is currently active. + #[must_use] + pub const fn active(self) -> bool { + return self.active; + } + + /// Returns the normalized horizontal coordinate. + #[must_use] + pub const fn x(self) -> f32 { + return self.x; + } + + /// Returns the normalized vertical coordinate. + #[must_use] + pub const fn y(self) -> f32 { + return self.y; + } +} + +impl Default for PointerState { + fn default() -> Self { + return Self::inactive(); + } +} + +#[cfg(test)] +#[path = "../unit_tests/pointer.rs"] +mod tests; diff --git a/crates/engines/engine-v1-common/unit_tests/input.rs b/crates/engines/engine-v1-common/unit_tests/input.rs index 93859a9..9f7ce83 100644 --- a/crates/engines/engine-v1-common/unit_tests/input.rs +++ b/crates/engines/engine-v1-common/unit_tests/input.rs @@ -1,5 +1,5 @@ // file: crates/engines/engine-v1-common/unit_tests/input.rs -// version: 1 +// version: 2 #[test] fn action_state_is_independent() { @@ -10,3 +10,11 @@ fn action_state_is_independent() { assert!(!input.is_active(crate::GameAction::Left)); }); } + +#[test] +fn pointer_state_is_carried_independently_from_actions() { + let pointer = crate::PointerState::normalized(true, 0.25, 0.75); + let input = crate::InputState::none().with_pointer(pointer); + assert_eq!(input.pointer(), pointer); + assert!(!input.is_active(crate::GameAction::Primary)); +} diff --git a/crates/engines/engine-v1-common/unit_tests/pointer.rs b/crates/engines/engine-v1-common/unit_tests/pointer.rs new file mode 100644 index 0000000..03e26b2 --- /dev/null +++ b/crates/engines/engine-v1-common/unit_tests/pointer.rs @@ -0,0 +1,10 @@ +// file: crates/engines/engine-v1-common/unit_tests/pointer.rs +// version: 1 + +#[test] +fn normalized_pointer_clamps_coordinates() { + let pointer = crate::PointerState::normalized(true, -0.5, 1.5); + assert!(pointer.active()); + assert_eq!(pointer.x(), 0.0); + assert_eq!(pointer.y(), 1.0); +} diff --git a/crates/engines/engine-v1-sdl/src/runtime.rs b/crates/engines/engine-v1-sdl/src/runtime.rs index 98b63ab..bb76da4 100644 --- a/crates/engines/engine-v1-sdl/src/runtime.rs +++ b/crates/engines/engine-v1-sdl/src/runtime.rs @@ -1,5 +1,5 @@ // file: crates/engines/engine-v1-sdl/src/runtime.rs -// version: 4 +// version: 5 /// Minimal SDL3 runtime used by Desktop POC runners. pub struct SdlRuntime { @@ -39,9 +39,10 @@ impl SdlRuntime { std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; let mut runner = engine_v1_common::FixedStepRunner::new(self.frame_duration); + let mut pointer = engine_v1_common::PointerState::inactive(); tracing::info!(title = %self.title, width = self.width, height = self.height, "SDL3 runtime started"); 'running: loop { - let mut input = engine_v1_common::InputState::none(); + let mut input = engine_v1_common::InputState::none().with_pointer(pointer); for event in events.poll_iter() { match event { sdl3::event::Event::Quit { .. } => break 'running, @@ -49,9 +50,20 @@ impl SdlRuntime { sdl3::event::Event::KeyDown { keycode: Some(sdl3::keyboard::Keycode::Space), .. } => { input = input.with_action(engine_v1_common::GameAction::Primary, true); }, + sdl3::event::Event::FingerDown { x, y, .. } | sdl3::event::Event::FingerMotion { x, y, .. } => { + pointer = engine_v1_common::PointerState::normalized(true, x, y); + input = input.with_pointer(pointer).with_action(engine_v1_common::GameAction::Primary, true); + }, + sdl3::event::Event::FingerUp { x, y, .. } | sdl3::event::Event::FingerCanceled { x, y, .. } => { + pointer = engine_v1_common::PointerState::normalized(false, x, y); + input = input.with_pointer(pointer); + }, _ => {}, } } + if pointer.active() { + input = input.with_pointer(pointer).with_action(engine_v1_common::GameAction::Primary, true); + } runner.tick(game, input); canvas.set_draw_color(sdl3::pixels::Color::RGB(18, 18, 24)); canvas.clear(); diff --git a/deltas/0.1.0/0-pre.8.md b/deltas/0.1.0/0-pre.8.md new file mode 100644 index 0000000..4a1c030 --- /dev/null +++ b/deltas/0.1.0/0-pre.8.md @@ -0,0 +1,128 @@ + + + +# Delta 0.1.0-0-pre.8 + +## Base + +Base validée : `0.1.0-0-pre.7.fix.1`. + +## Validation du delta précédent + +`0.1.0-0-pre.7.fix.1` a été validée le 2026-09-16. + +Les gates légères, les deux builds Rust Android, la présence des bibliothèques natives et les deux builds APK sont propres. + +`adb` est installé et fonctionnel, mais aucun appareil ou émulateur n'était attaché. Le smoke runtime Android n'était donc pas requis pour accepter ce jalon. + +## Objectifs + +- introduire un input tactile portable dans le moteur ; +- utiliser les événements SDL3 Android plutôt qu'un bridge JNI pour les touch events ; +- rendre le bridge Java/JNI minimal réellement vérifiable ; +- conserver une exception FFI Rust strictement bornée. + +## Input tactile + +`engine-v1-common` introduit `PointerState` : + +- état actif/inactif ; +- coordonnées normalisées `x` et `y` ; +- bornage dans `[0.0, 1.0]`. + +`InputState` transporte désormais ce pointeur. + +`engine-v1-sdl` maintient l'état tactile entre les frames : + +- `FingerDown` et `FingerMotion` activent/mettent à jour le pointeur ; +- `FingerUp` et `FingerCanceled` le désactivent ; +- un pointeur actif active également `GameAction::Primary` dans cette baseline. + +## Bridge Java/JNI + +`NativeBridge` expose : + +```java +public static native int nativeContractVersion(); +``` + +La bibliothèque Rust exporte le symbole JNI correspondant et retourne `1`. + +`SaseGameActivity.onCreate()` appelle `NativeBridge.requireCompatibleContract()` après l'initialisation SDL. + +Le bridge est ainsi réellement vérifié au premier lancement Android disponible. + +## Exception FFI + +L'exception Rust du crate `game-android-entrypoint` autorise désormais exactement deux exports : + +- `SDL_main` ; +- `Java_com_sasedev_games_common_NativeBridge_nativeContractVersion`. + +Les blocs `unsafe`, fonctions `unsafe`, déréférencements de pointeurs bruts et autres attributs unsafe restent interdits. + +## Validation Rust à exécuter + +Le delta modifie du Rust : + +```bash +cargo fmt --all +cargo fmt --all -- --check + +python3 scripts/audit_rust_workspace_rules.py +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates Android deltas history + +cargo check --workspace +cargo clippy --workspace --all-targets --all-features -- -D warnings +``` + +Tests ciblés : + +```bash +cargo test -p engine-v1-common --all-targets --all-features +cargo test -p engine-v1-sdl --all-targets --all-features +cargo test -p game-android-entrypoint --no-default-features +``` + +## Validation Android + +Reconstruire les bibliothèques natives : + +```bash +python3 scripts/build_android_rust.py reflex +python3 scripts/build_android_rust.py snake +``` + +Puis les APK : + +```bash +cd Android +gradle :game-reflex-poc:assembleDebug +gradle :game-snake-poc:assembleDebug +cd .. +``` + +## Smoke appareil + +Si un appareil ou émulateur est attaché : + +```bash +adb devices +``` + +installer puis lancer les deux applications. + +Critères supplémentaires de ce delta : + +- aucun `UnsatisfiedLinkError` sur `NativeBridge.nativeContractVersion` ; +- aucune erreur de contrat JNI ; +- un contact tactile ne provoque aucun crash ; +- SDL3 continue à recevoir les événements tactiles. + +Si aucun device n'est disponible, les builds peuvent valider le delta mais le smoke runtime reste explicitement en attente dans l'historique. + +## Règle de transition + +Si les gates Rust et Android sont propres, passer automatiquement à `0.1.0-0-pre.9`. + +En cas d'échec imputable au projet, produire `0.1.0-0-pre.8.fix.1`. diff --git a/docs/000-README.md b/docs/000-README.md index 19cb1d4..67bd0ba 100644 --- a/docs/000-README.md +++ b/docs/000-README.md @@ -1,5 +1,5 @@ - + # Documentation games.sasedev @@ -51,6 +51,7 @@ Voir [`../RULES.md`](../RULES.md), notamment [`rules/RULES_COMMANDS.md`](rules/R - [`development/004-SDL3_DESKTOP_PREREQUISITES.md`](development/004-SDL3_DESKTOP_PREREQUISITES.md) — prérequis SDL3 Desktop, stratégie de liaison système et vérification `pkg-config`. - [`development/005-DESKTOP_WINDOW_POLICY.md`](development/005-DESKTOP_WINDOW_POLICY.md) — taille initiale, redimensionnement et séparation future entre fenêtre physique et résolution virtuelle. - [`development/006-ANDROID_RUST_NATIVE_BUILD.md`](development/006-ANDROID_RUST_NATIVE_BUILD.md) — build `cdylib` Rust Android, cargo-ndk, packaging `jniLibs` et symbole `SDL_main`. +- [`development/007-ANDROID_JNI_BRIDGE.md`](development/007-ANDROID_JNI_BRIDGE.md) — frontière Java/JNI minimale, version de contrat et séparation avec l'input SDL3. ## Historique validé diff --git a/docs/architecture/004-INPUT_AND_CONTROLS.md b/docs/architecture/004-INPUT_AND_CONTROLS.md index d37d817..f19fd4c 100644 --- a/docs/architecture/004-INPUT_AND_CONTROLS.md +++ b/docs/architecture/004-INPUT_AND_CONTROLS.md @@ -1,5 +1,5 @@ - + # Abstraction des entrées et contrôles @@ -53,3 +53,17 @@ Cette structure n'est pas imposée comme API définitive à la baseline ; elle i ## Orientation d'écran Portrait convient particulièrement aux jeux one-button, merge, puzzle, idle, stacking et climber. Paysage convient mieux aux shooters, runners latéraux, survivor-like, tower defense et jeux utilisant deux zones de contrôle. + +## Pointeur primaire normalisé + +Le moteur V1 expose un `PointerState` indépendant de la plateforme : + +- `active` indique qu'un pointeur primaire est maintenu ; +- `x` et `y` sont normalisés dans `[0.0, 1.0]` ; +- la plateforme borne les coordonnées avant de les transmettre au gameplay. + +SDL3 fournit cette abstraction sur Android avec les événements `FingerDown`, `FingerMotion`, `FingerUp` et `FingerCanceled`. + +La baseline `0.1.0-0-pre.8` mappe également un doigt actif vers `GameAction::Primary`. Cette association est volontairement minimale et pourra être remplacée par des zones tactiles ou boutons virtuels par jeu. + +Le gameplay ne dépend d'aucun type Android, Java ou SDL3. diff --git a/docs/development/007-ANDROID_JNI_BRIDGE.md b/docs/development/007-ANDROID_JNI_BRIDGE.md new file mode 100644 index 0000000..1c96319 --- /dev/null +++ b/docs/development/007-ANDROID_JNI_BRIDGE.md @@ -0,0 +1,47 @@ + + + +# Bridge Java/JNI Android + +## Objectif + +Le bridge Java/JNI sert uniquement aux services réellement Android qui ne sont pas déjà fournis de manière portable par SDL3. + +L'input tactile de base ne passe pas par JNI : SDL3 fournit directement les événements de doigt et le moteur les convertit en `PointerState`. + +## Contrat V1 + +La baseline expose un unique appel Java vers Rust : + +```text +NativeBridge.nativeContractVersion() -> 1 +``` + +`SaseGameActivity` vérifie ce contrat après `SDLActivity.onCreate()`. + +Cette vérification détecte au démarrage : + +- bibliothèque Rust non chargée ; +- symbole JNI absent ; +- divergence de version de bridge. + +## Extension future + +Les futurs services pourront ajouter des opérations dédiées pour : + +- haptique Android ; +- partage natif ; +- facturation ; +- publicité ; +- notifications ; +- autres services dépendants du framework Android. + +Ils ne doivent pas servir à réimplémenter les entrées, fenêtres ou événements déjà couverts par SDL3. + +## Sécurité Rust + +L'export JNI utilise un symbole nommé explicitement et reçoit deux pointeurs opaques JNI qui ne sont pas déréférencés. + +Aucun bloc `unsafe` ni fonction `unsafe` n'est autorisé. + +Si un futur service nécessite de manipuler réellement `JNIEnv`, il devra faire l'objet d'une tranche distincte avec une politique FFI réévaluée plutôt que d'élargir implicitement l'exception actuelle. diff --git a/docs/rules/RULES_RUST.md b/docs/rules/RULES_RUST.md index 585b455..05b5446 100644 --- a/docs/rules/RULES_RUST.md +++ b/docs/rules/RULES_RUST.md @@ -1,5 +1,5 @@ - + # Règles Rust générales @@ -8,7 +8,7 @@ - **RUST-BASE-001** — L'édition Rust est Rust 2024. - **RUST-BASE-002** — Chaque `lib.rs` et `main.rs` active `missing_docs`, `unreachable_pub` et interdit `unsafe_code`, sauf exception FFI explicitement définie par cette règle. - **RUST-BASE-003** — Les lints communs sont déclarés au workspace et hérités par les crates ; une crate FFI exceptée peut déclarer localement le même profil avec uniquement le niveau `unsafe_code` ajusté. -- **RUST-BASE-004** — Les blocs `unsafe` et fonctions `unsafe` sont interdits. L’unique exception actuelle est `crates/apps/game-android-entrypoint/src/lib.rs`, autorisé à porter exactement un attribut `#[unsafe(export_name = "SDL_main")]` afin d’exposer le symbole exigé par SDL Android. Cette exception n’autorise aucun déréférencement de pointeur brut ni autre attribut unsafe. +- **RUST-BASE-004** — Les blocs `unsafe` et fonctions `unsafe` sont interdits. L’unique exception actuelle est `crates/apps/game-android-entrypoint/src/lib.rs`, autorisé à porter uniquement les attributs `#[unsafe(export_name = "SDL_main")]` et `#[unsafe(export_name = "Java_com_sasedev_games_common_NativeBridge_nativeContractVersion")]` nécessaires aux frontières SDL Android et JNI. Cette exception n’autorise aucun déréférencement de pointeur brut, aucun autre attribut unsafe, aucun bloc `unsafe` et aucune fonction `unsafe`. - **RUST-BASE-005** — Tout fichier Rust possède les en-têtes `// file: ...` et `// version: N`. - **RUST-DEP-001** — Une dépendance tierce partagée déclare uniquement sa contrainte de version canonique sous `[workspace.dependencies]`, sauf exception normative explicitement documentée. - **RUST-DEP-002** — Les features d'une dépendance tierce sont activées dans le `Cargo.toml` de la crate qui en a réellement besoin, via `workspace = true`; elles ne sont pas activées globalement au workspace par commodité. diff --git a/history/0.1.0/0-pre.7.fix.1.md b/history/0.1.0/0-pre.7.fix.1.md new file mode 100644 index 0000000..9210b2e --- /dev/null +++ b/history/0.1.0/0-pre.7.fix.1.md @@ -0,0 +1,32 @@ + + + +# Historique 0.1.0-0-pre.7.fix.1 + +## Statut + +Validé par l'utilisateur le 2026-09-16. + +## Gates validées + +- `cargo fmt --all -- --check` ; +- audits Python du workspace et des tableaux Markdown ; +- `cargo check --workspace` ; +- build Rust Android Reflex ; +- build Rust Android Snake ; +- présence de `libSDL3.so` et `libgame_android_entrypoint.so` pour les deux applications ; +- assemblage Gradle Reflex ; +- assemblage Gradle Snake. + +## Toolchain observée + +- `cargo-ndk 4.1.2` ; +- target Rust `aarch64-linux-android` ; +- NDK r28c installé ensuite ; +- `adb 1.0.41 / platform-tools 37.0.1`. + +Aucun appareil ou émulateur n'était attaché lors de la validation ; le smoke runtime Android reste donc à exécuter lorsqu'un device sera disponible. + +## Portée durable + +Ce jalon valide la production du `cdylib` Rust Android, le packaging de SDL3 et de la bibliothèque applicative dans les APK et la chaîne de build Android native. diff --git a/scripts/audit_rust_general_rules.py b/scripts/audit_rust_general_rules.py index 1e1b1e6..de88080 100755 --- a/scripts/audit_rust_general_rules.py +++ b/scripts/audit_rust_general_rules.py @@ -520,11 +520,18 @@ def audit_file(root: pathlib.Path, path: pathlib.Path) -> list[Violation]: if attribute not in lines[:24]: violations.append(Violation("RUST-BASE-103", relative, 1, f"missing `{attribute}`")) if ffi_export_exception: - export_attribute = '#[unsafe(export_name = "SDL_main")]' - if text.count(export_attribute) != 1: - violations.append(Violation("RUST-FFI-101", relative, 1, "Android entrypoint must contain exactly one SDL_main export attribute")) + required_export_attributes = ( + '#[unsafe(export_name = "Java_com_sasedev_games_common_NativeBridge_nativeContractVersion")]', + '#[unsafe(export_name = "SDL_main")]', + ) + for export_attribute in required_export_attributes: + if text.count(export_attribute) != 1: + violations.append(Violation("RUST-FFI-101", relative, 1, f"Android entrypoint must contain exactly one required export attribute: {export_attribute}")) + unsafe_export_count = text.count("#[unsafe(export_name = ") + if unsafe_export_count != len(required_export_attributes): + violations.append(Violation("RUST-FFI-103", relative, 1, "Android entrypoint contains an unexpected unsafe export attribute")) if "unsafe {" in text or re.search(r"\bunsafe\s+fn\b", text) is not None or re.search(r"\bunsafe\s+extern\b", text) is not None: - violations.append(Violation("RUST-FFI-102", relative, 1, "Android entrypoint exception permits an unsafe export attribute only; unsafe blocks/functions remain forbidden")) + violations.append(Violation("RUST-FFI-102", relative, 1, "Android entrypoint exception permits named unsafe export attributes only; unsafe blocks/functions remain forbidden")) exports: list[tuple[int, str]] = [] for idx, line in enumerate(lines, 1): stripped = line.strip()