Git Product home page Git Product logo

crux's Introduction

Crux · GitHub license Crate version Docs Build status

Cross-platform app development in Rust

  • Shared Core for Behavior - Crux helps you share your app's business logic and behavior across mobile (iOS/Android) and web — as a single reusable core built with Rust.
  • Thin Shell for UI - Crux recognizes that the best experiences are built with modern declarative frameworks such as SwiftUI, Jetpack Compose, React/Vue, or a WebAssembly based framework (like Yew) — however, it aims to keep this UI layer as thin as it can be, with all other work done by the shared core.
  • Type Generation - the interface with the core has static type checking across languages — types and serialization code are generated for Swift, Kotlin and TypeScript. Rust shells can import the core directly.
  • Capabilities - capabilities express the intent for side effects such as calling an API. Because all side effects (including UI) are performed by the shell, the core becomes trivial to test comprehensively — test suites run in milliseconds (not in minutes or hours).

Getting Started

Learn how to use Crux in your project.

Follow the readme in the project's repository on Github.

Read the API documentation

Watch the introductory talk at the recent Rust Nation 2023 conference in London.

You can also join the friendly conversation on our Zulip channel.

Note, that Crux is experimental and currently under active development (probably not ready for use in production apps just yet). However, the master branch should always be working well, and we will try to keep the examples and documentation up to date as we go. We do think that the API has now settled, so have a play! :-)

Architectural Overview

Logical architecture

The fundamental architectural concept is the strict separation of pure computational tasks from tasks that cause side effects. This is similar to the way Elm works.

Side-effect-free core

In the above diagram, the inner "Core" is compiled and linked to the outer "Shell" on each platform as a library:

  • On iOS as a native static library
  • On Android as a dynamic library using Java Native Access
  • In a browser as a WebAssembly module

In fact, because WebAssembly (Wasm) is one of the compilation targets, the core must remain side-effect free, due to the sandboxed nature of the Wasm runtime environment.

As such, the core is completely isolated and secure against software supply-chain attacks, as it has no access to any external APIs. All it can do is perform pure calculations and keep internal state.

Following the Elm architecture, the core defines the key component types within the application:

  • Event — an enum describing the events which the core can handle
  • Model — describes the internal state of the application
  • ViewModel — represents information that should be displayed to the user

The former two are tied together by the update function, familiar from Elm, Redux or other event sourcing architectures, which currently has this type signature:

fn update(
    &self,
    event: Event,
    model: &mut Model,
    capabilities: &Capabilities,
)

The job of the update function is to process an Event, update the model accordingly, and potentially request some side-effects using capabilities.

Application Shell

The enclosing platform native "Shell" is written using the language appropriate for the platform, and acts as the runtime environment within which all the non-pure tasks are performed. From the perspective of the core, the shell is the platform on which the core runs.

Communication Between the Application Shell and the Core

Following the Elm architecture, the interface with the core is message based. This means that the core is unable to perform anything other than pure calculations. To perform any task that creates a side-effect (such as an HTTP call or random number generation), the core must request it from the shell.

The core has a concept of Capabilities — reusable interfaces for common side-effects — supporting fire-and-forget, request/response, and streaming semantics.

The only built-in capability is Render. But this repository contains a few capabilities at various stages of maturity, and you can easily write your own if you want to:

  1. Render (ask UI to render the ViewModel) — source, built-in to crux_core, request only
  2. Http (full HTTP implementation based on the Surf API) — source, crate, request/response
  3. KeyValue (very basic key-value store API) — source, crate, request/response
  4. Time (get the current time) — source, crate, request/response
  5. Platform (get the current platform) — source, crate, request/response
  6. SSE (basic Server-Sent Events) — source, request/streaming
  7. PubSub (pub sub with streaming) — source, request/response/streaming
  8. Timer (timer start, finish, cancel) — source, request/response/streaming
  9. Delay — part of tutorial in the book

crux

This means the core interface is simple:

  • process_event: Event -> Vec<Request> - processes a user interaction event and potentially responds with capability requests. This is the API for the driving side in the above diagram.
  • handle_response: (uuid, SomeResponse) -> Vec<Request> - handles the response from the capability and potentially follows up with further requests. This is the API for the driven side in the above diagram.
  • view: () -> ViewModel - provides the shell with the current data for displaying user interface

Updating the user interface is considered a side-effect and is provided by the built-in Render capability.

This design means the core can be tested very easily, without any mocking and stubbing, by simply checking the Input/Output behaviour of the three functions.

Foreign Function Interface

The Foreign Function Interface allowing the shell to call the above functions is provided by Mozilla's UniFFI on a mobile device, or in the browser, by wasm-pack.

In order to both send more complex data than UniFFI currently supports, and enforce the message passing semantics, all messages are serialized, sent across the boundary, then deserialized using serde-generate which also provides type generation for the foreign (non-Rust) languages.

This means that changes to types in the core, especially the Event and Request types, propagate out into the shell implementations and cause type errors where appropriate (such as an exhaustive match on an enum check).

Message Types

Three types of message are exchanged between the application and the core.

  • Messages of type Event are sent from the Shell to the Core in response to an event happening in the user interface (the driving side). They start a potential sequence of further message exchanges between the shell and the core. Messages are passed on unchanged.
  • Messages of type Request are sent from the Core to the Shell to request the execution of some side-effect-inducing task. The Core responds with zero or more Request messages after receiving an Event message (the driven side).
  • Response messages are sent from the Shell to the Core carrying the result of an earlier request.

Request messages contain the inputs for the requested side-effect, along with a uuid used by the core to pair requests and their responses together. The exact mechanics are not important, but it is important for the request's uuid to be passed on to the corresponding response.

Typical Message Exchange Cycle

A typical message exchange cycle may look like this:

  1. User interaction occurs in the Shell, which results in an event
  2. The Shell handles this event by constructing an Event
  3. The Shell calls the Core's process_event function passing the Event as an argument
  4. The Core performs the required processing, updating both its inner state and the view model
  5. The Core returns one or more Request messages to the Shell

In the simplest case, the Core will respond to an Event by returning the single Request - render.

This requests that the Shell re-renders the user interface. When Render is the only response from the Core, the message cycle has completed and the Core has now "settled".

In more complex cases however, the Core may well return multiple Requests; each of which instructs the Shell to perform a side-effect-inducing task such as:

  • Make a network call, or
  • Fetch the current date/time stamp, or
  • Perform biometric authentication, or
  • Obtain an image from the camera, or
  • Whatever else you can think of...

Many of these side-effecting-inducing tasks are asynchronous. The Shell is responsible for passing responses back to the core (to the handle_response function), which may respond with further requests.

This exchange continues until the core stops requesting further side-effects (typically the last side-effect requested would be Render).

Run the Counter Example locally

Refer to examples/counter README

How to Start Your Own New Project

Refer to the Getting Started section of the tutorials.


Sponsors

Crux is kindly sponsored by the following organizations. Your help is very much appreciated.


Red Badger Consulting Limited

Red Badger logo

Red Badger is the digital product consultancy trusted by blue chips and global brands. Our product design and technical pedigree allow us to craft high-impact digital products customers want. We use modern engineering approaches to deliver sustainable change. And embed digital capabilities to power continuous innovation.


Zulip

Zulip round icon

Zulip is an open-source modern team chat app designed to keep both live and asynchronous conversations organized.

Zulip sponsor Crux by providing our Zulip server — thank you Zulip!


crux's People

Contributors

andrewweston avatar angusp avatar carltongibson avatar charypar avatar chriswhealy avatar dominicd avatar eventualbuddha avatar gianpaj avatar gongbaodd avatar matt-thomson avatar mmannes avatar niall-rb avatar nmrshll avatar obmarg avatar ruiramos avatar scholtzan avatar spotys avatar stuartharris avatar trapfether avatar wasnotrice avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

crux's Issues

Question about adding translation resources from Core to Shell

Hi Crux team. Great project! I'm looking for a way to provide the Shell with translations. How do you think it should be exposed?
I already set the language in the Core from the app settings and in the Core there is "tr!" macro that handles the translations.
This works for the translations that are a part of the ViewModel. But there are also translations that are a part of the interface itself (Button names, sections names etc.) and also enum variants translations.

I had a couple of ideas:

  1. Add additional entry point to expose synchronous functions from the Core - this is like a reverse capability - this would mean adding some new function to the UDL.
  2. Use an Event to request the translation from the Shell and then maybe KV capability to store it? Then read it from KV in the Shell? - this seems like a very round about way but it would work without any changes to crux.
  3. Share the translation resources in both Core and Shell - this I don't like because I would have to guarantee that the Shell has the same possibility to use https://projectfluent.org.
  4. I was also playing around with some custom serialisers for enums that would convert the variant name into translated name which could actually work but only for enums.

Generally I tend to lean towards the situation where the Core is responsible for translations.

I would be grateful for some guidance. I could contribute some solution if you find any of the above interesting.

Bevy and Crux similarities

Hi again! I see a lot of similarities between Bevy and Crux if I squint my eyes really hard.

One example are capabilities both Bevy and Crux are meant to be running in different environments.
For example the Multi-platform Key Value store implementation is written by someone in the bevy community - https://github.com/johanhelsing/bevy_pkv. It depends on bevy ecs so it is not a perfect dependency for Crux but it almost can be.

Perhaps Crux can also draw inspiration from the way Bevy defines plugins. Plugins in Bevy are almost standalone apps with their own systems and data. I feel that capabilities are very similar to plugins and a capability that is defined and used in the Core can also have a bundled implementation in the Shell. So Rust can provide not only compiled Core library but also compiled capabilities implementations that live outside the Core in the Shell.

The bridge should allow other other serialisers than bincode

The bridge currently assumes and only supports bincode serialisation. This is not desirable in every situation, and it should be possible to make the bridge generic over the serializer, to allow, for example, JSON serialization over the boundary when needed.

Async combinators for capabilities

Sometimes, it's necessary to request multiple APIs to display something on the screen. For instance:

cap.http(STORIES, Event::Stories)
cap.http(FEED, Event::Feed)

However, we need to ensure that nothing is displayed until both results are returned. There are two main reasons for this: data consistency and rendering efficiency. Data consistency ensures that we display a complete view of the screen, without jumping between different states. Rendering efficiency is important because we don't want to re-render the screen twice.

I don't know how to address this, but probably we need something like combinators that allow us to express concurrent capabilities. An example of this would be:

cap.join!(cap.http(STORIES), cap.http(FEED), Event::StoriesAndFeed)

Edit: As @charypar pointed, we can avoid emitting render in the "mid-state", until both tasks are completed. On the other hand, this may lead to conditional rendering inside n calls, so perhaps it's better should be phrased as an ergonomic problem, not rendering problem.

Getting started instructions fail as missing workspace members

I am all for including the source rather than using MD examples of code that might be missed in testing that are used in https://github.com/redbadger/crux/blob/master/docs/src/getting_started/core.md but sadly the cargo workspace includes members that are not yet present when trying to build shared with cargo run -p shared --bin uniffi-bindgen

I am not sure if it's worth maintaining a step-wise version that comments out or omits the members... but maybe worth mentioning in the walkthrough?

Create and document an example with multiple independent cores

Nothing should technically stop an application to have multiple independent Crux cores, but this use is not well tested and isn't documented at all.

  1. Build an example (e.g. two independent counters) covering the key shells (iOS, Android and Web)
  2. Document the build configuration

Multiple windows in MacOS, iPad apps (and perhaps Android, etc...)

Hi,

Are there any known strategies for working with Crux when multiple windows are opened in MacOS and/or iPad apps (maybe even iOS apps)?

As I understand it, each window gets its own state and I was curious if crux has been used in this manner (multiple views) and what I should look out for (if anything).

Any advice greatly appreciated!

How to handle errors in capabilities?

Hi, I am reading the docs and it isn't apparent how one should handle errors when writing custom capabilities. Do you all have a preferred architecture?

For example, I have a db capability that is pretty much ripped from the simple capabilities in crux (kv, time, etc - I posted the code below). Currently my db capability is such that the DbResponse is always successful (pub struct DbResponse(pub Vec<u8>); - see below). But really, I'd like to establish some way to handle errors so I am thinking something along the lines of changing my response to something like:

use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct DbError {
    statement: String,
    params: Vec<String>,
    error_code: u32,
    error_message: String,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
enum DbResponse {
    Data(Vec<u8>),
    Error(DbError),
}

Anyway, wondering what your thoughts are on the matter. Any advice greatly appreciated...

Thanks!

The simple db capability...

use crux_macros::Capability;
use serde::{Deserialize, Serialize};

use crux_core::capability::{CapabilityContext, Operation};
use serde::de::DeserializeOwned;

/// Capability ripped from https://github.com/redbadger/crux/blob/master/crux_kv/src/lib.rs

#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct DbRequest {
    pub statement: String,
    pub params: Vec<String>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DbResponse(pub Vec<u8>);

impl Operation for DbRequest {
    type Output = DbResponse;
}

#[derive(Capability)]
pub struct Db<Ev> {
    context: CapabilityContext<DbRequest, Ev>,
}

impl<Ev> Db<Ev>
where
    Ev: 'static,
{
    pub fn new(context: CapabilityContext<DbRequest, Ev>) -> Self {
        Self { context }
    }

    pub fn run<F, T>(&self, statement: &str, params: Vec<String>, make_event: F)
    where
        F: Fn(T) -> Ev + Send + Sync + 'static,
        T: DeserializeOwned,
    {
        let ctx = self.context.clone();
        let statement = statement.to_string();
        self.context.spawn(async move {
            let DbResponse(res) = ctx
                .request_from_shell(DbRequest { statement, params })
                .await;
            // TODO handle a DbResponse that is an error...
            let t: T = serde_json::from_slice(res.as_slice()).unwrap();
            ctx.update_app(make_event(t));
        });
    }
}

Capability derive macro not able to send NewEv between threads

Implementing the Delay capability as described by the guide (https://redbadger.github.io/crux/guide/capability_apis.html) the compiler showed the following error:

error[E0277]: `NewEv` cannot be sent between threads safely
  --> shared/src/app.rs:73:10
   |
73 | #[derive(Capability)]
   |          ^^^^^^^^^^ `NewEv` cannot be sent between threads safely
   |
note: required by a bound in `Delay::<Ev>::new`
  --> shared/src/app.rs:80:19
   |
80 |     Ev: 'static + Send
   |                   ^^^^ required by this bound in `Delay::<Ev>::new`
81 | {
82 |     pub fn new(context: CapabilityContext<DelayOperation, Ev>) -> Self {
   |            --- required by a bound in this associated function

The function map_events should require the Send trait for the NewEv generic, but it doesn't:

            fn map_event<F, NewEv>(&self, f: F) -> Self::MappedSelf<NewEv>
            where
                F: Fn(NewEv) -> Ev + Send + Sync + Copy + 'static,
                Ev: 'static,
                NewEv: 'static,
            {
              #name::new(self.context.map_event(f))
            }

Could not pnpm install: Os

Hi,

I am trying to build the counter example, and I am getting the error below:

error: failed to run custom build command for `shared_types v0.1.0 (/crux/examples/simple_counter/shared_types)`

Caused by:
  process didn't exit successfully: `/crux/examples/simple_counter/target/debug/build/shared_types-b6ac82c2067f0340/build-script-build` (exit status: 101)
  --- stdout
  cargo:rerun-if-changed=../shared

  --- stderr
  thread 'main' panicked at /.cargo/registry/src/index.crates.io-6f17d22bba15001f/crux_core-0.6.5/src/typegen.rs:484:14:
  Could not pnpm install: Os { code: 2, kind: NotFound, message: "No such file or directory" }
  note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Seems like I am missing a something?

typegen issue with crux_http 0.7.0

I get the following error when generating types after upgrading to crux_http 0.7.0:

--- stderr
Error: type tracing failed: Not supported: deserialize_any:

An unsupported callback was called during (de)serialization tracing. In practice, this happens when an
unsupported Serde attribute is used. Attributes specific to self-describing formats (JSON, YAML, TOML)
are generally not supported. This includes: `#[serde(flatten)]`, `#[serde(tag = "type")]`, 
`#[serde(tag = "t", content = "c")]`, and `#[serde(untagged)]`.

I can trace it to the A::Capabilities::register_types(self) method but haven't had the time to dig further into it (yet).

Upgrading the counter example to use 0.7.0 gives the same issue.

I'm happy to pursue if the answer isn't immediately obvious.

Custom code generation from `serde-reflection` metadata

Current codegen for Swift, Kotlin and TypeScript relies on serde-generate. This works quite well, but has a few limitations/challenges:

  1. Generics support across the FFI boundary (especially for Option<T> and Result<T>) - mostly because discovering a generic type with serde is non-trivial and probably not exactly reliable
  2. Generated TypeScript code handling of enums could be improved on, especially enums could be more idiomatic
  3. Adding support for other shell languages is challenging. The obvious example is Dart in order to support Flutter

Crux doctor

CLI tool

Some ideas for a simple Crux CLI that can analyze, validate, fix, and create Crux projects within a workspace. Let's start simple, and evolve over time.

Some ideas:

crux doctor
# Lists files not matching templates

crux doctor diff [file]
# Shows the diff between file and template

crux doctor fix [file]
# Replaces file with template

crux doctor fix --missing
# Adds files which are wholy missing compared with the template

crux init
# Creates a Crux.toml file with the basic configuration - a single core and type-gen crate
# Runs `crux doctor fix --missing`

crux new [path]
# Same as `init` but in a specified directory

crux add --shell [name] [template] --cores [core_a core_b ...]
# Adds the configuration into crux.toml
# Runs `crux doctor fix --missing`

To do list

  • Doctor
    • Parse, validate, rewrite Crux.toml
    • Fill in the core template correctly
      • Fill in core name
    • Fill in the shell template correctly
      • Choose the right template file(s) (e.g. iOS)
      • Fill in the shell name (in the right format, e.g. camel case)
      • Fill in the relevant core linking configuration for each linked core
    • Diff reality vs template "ideal"
      • Display full diff
      • Display diff --paths-only
      • Display diff for a specific file (or path prefix?)
    • fix - Replace file with template generated file
  • init / new / add
    • Edit the Crux.toml
  • Later?
    • Interactive doctor

Is it possible to have a hybrid shell e.g. Rust+Swift/Kotlin

I have an iOS project that is already using Rust libraries via FFI.

I have been looking for a solution to better structure this App as it grows. I'm expecting to integrate more Rust libraries.

I took this route to simplify migrating the App to Android and Desktop.

Crux looks like a great solution for the functional core.

The rust libraries I am currently using provide database and file sync capabilities.

I can support the capabilities in Swift and route Requests to the third party Rust libraries where necessary.

This does not seem optimal, is there any support for a hybrid shell with some requests getting routed to rust implementations and some to Swift/Kotlin?

Guide to using Crux with Flutter

There are no docs for using Crux with Flutter. Based on Crux's architecture, this should be relatively easy, since Flutter can take over UI, replacing 3 platform languages and their respective UI frameworks, allowing Crux to handle the core of the logic in the app. All Flutter would have to do is react to and trigger events. Would you have any thoughts on how to do this / get started?

counter example installation fails due to missing pnpm

Following the steps here:
https://github.com/redbadger/crux/blob/master/examples/counter/README.md

Got an error at step 3:

 Blocking waiting for file lock on build directory
 Compiling getrandom v0.2.8
 Compiling rand_core v0.6.4
 Compiling uuid v1.3.0
 Compiling rand_chacha v0.3.1
 Compiling rand v0.8.5
 Compiling phf_generator v0.10.0
 Compiling phf_macros v0.10.0
 Compiling phf v0.10.1
 Compiling serde-generate v0.25.0
 Compiling crux_core v0.3.0 (/Users/guofoo/git/crux/crux/crux_core)
 Compiling crux_http v0.2.0 (/Users/guofoo/git/crux/crux/crux_http)
 Compiling crux_platform v0.1.1 (/Users/guofoo/git/crux/crux/crux_platform)
 Compiling crux_kv v0.1.1 (/Users/guofoo/git/crux/crux/crux_kv)
 Compiling crux_time v0.1.1 (/Users/guofoo/git/crux/crux/crux_time)
 Compiling shared v0.1.0 (/Users/guofoo/git/crux/crux/examples/counter/shared)
 Compiling shared_types v0.1.0 (/Users/guofoo/git/crux/crux/examples/counter/shared_types)
error: failed to run custom build command for `shared_types v0.1.0 (/Users/guofoo/git/crux/crux/examples/counter/shared_types)`
Caused by:
  process didn't exit successfully: `/Users/guofoo/git/crux/crux/examples/counter/target/debug/build/shared_types-07e3345a7cc49a79/build-script-build` (exit status: 101)
  --- stdout
  cargo:rerun-if-changed=../shared
  --- stderr
  thread 'main' panicked at 'Could not pnpm install: Os { code: 2, kind: NotFound, message: "No such file or directory" }', /Users/guofoo/git/crux/crux/crux_core/src/typegen.rs:252:14
  note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
➜ ~/git/crux/crux/examples/counter/shared_types :(master)

_
So I installed pnmp and ran cargo run again, and everything proceeded normally.

Android mergeDexRelease task fails

This is the first time I've tried building a signed Android AAB since I updated from crux_core 0.4.0 to 0.6.0, and it seems like something broke in the typegen. I'm no longer able to build signed release AABs, although debug builds still work as expected.

I'm getting this error in the :app:mergeDexRelease task, even after cleaning the project (removing the build directories in question):

Type com.novi.serde.ArrayLen is defined multiple times: /Users/eric/code/my_project/android/shared/build/.transforms/f9ef25802a86e432ac275520f2e2daf4/transformed/release/com/novi/serde/ArrayLen.dex, /Users/eric/code/my_project/android/app/build/intermediates/external_libs_dex/release/mergeExtDexRelease/classes3.dex

I've never seen something like this before, and I'm not sure what to make of it. I do see entries in both of these places, but it looks like this is only failing on ArrayLen because it's the first one—every type is defined in both of these places. Any ideas what might be causing this?

[iOS] building to device issues - Library not loaded

I'm getting an error when building to the device. The build is successful but on boot, I get this error.

error message:

dyld[669]: Library not loaded: /Users/romerorickyio/Developer/crux/apps/crux-submodule-app/target/aarch64-apple-ios/debug/deps/libshared.dylib
  Referenced from: <_ID_> /private/var/containers/Bundle/Application/_ID_/iOS.app/iOS
  Reason: tried: '/usr/lib/system/introspection/libshared.dylib' (errno=2, not in dyld cache), '/Users/romerorickyio/Developer/crux/apps/crux-submodule-app/target/aarch64-apple-ios/debug/deps/libshared.dylib' (errno=2), '/private/preboot/Cryptexes/OS/Users/romerorickyio/Developer/crux/apps/crux-submodule-app/target/aarch64-apple-ios/debug/deps/libshared.dylib' (errno=2), '/Users/romerorickyio/Developer/crux/apps/crux-submodule-app/target/aarch64-apple-ios/debug/deps/libshared.dylib' (errno=2), '/usr/local/lib/libshared.dylib' (errno=2), '/usr/lib/libshared.dylib' (errno=2, not in dyld cache)
Library not loaded: /Users/romerorickyio/Developer/crux/apps/crux-submodule-app/target/aarch64-apple-ios/debug/deps/libshared.dylib
  Referenced from: <_ID_> /private/var/containers/Bundle/Application/_ID_/iOS.app/iOS
  Reason: tried: '/usr/lib/system/introspection/libshared.dylib' (errno=2, not in dyld cache), '/Users/romerorickyio/Developer/crux/apps/crux-submodule-app/target/aarch64-apple-ios/debug/deps/libshared.dylib' (errno=2), '/private/preboot/Cryptexes/OS/Users/romerorickyio/Developer/crux/apps/crux-submodule-app/target/aarch64-apple-ios/debug/deps/libshared.dylib' (errno=2), '/Users/romerorickyio/Developer/crux/apps/crux-submodule-app/target/aarch64-apple-ios/debug/deps/libshared.dylib' (errno=2), '/usr/local/lib/libshared.dylib' (errno=2), '/usr/lib/libshared.dylib' (errno=2, not in dyld cache)
dyld config: DYLD_LIBRARY_PATH=/usr/lib/system/introspection DYLD_INSERT_LIBRARIES=/usr/lib/libBacktraceRecording.dylib:/usr/lib/libMainThreadChecker.dylib:/usr/lib/libRPAC.dylib:/Developer/Library/PrivateFrameworks/DTDDISupport.framework/libViewDebuggerSupport.dylib

I have the LIBRARY_SEARCH_PATHS setup.

"LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios/debug";

Screenshot 2023-03-23 at 2 32 13 PM

Both target/aarch64-apple-ios/debug/deps/libshared.dylib' & target/aarch64-apple-ios/debug/libshared.dylib'files are in the target folder..

Screenshot 2023-03-23 at 2 32 42 PM

I don't get an error when building to the sim.

Type com.novi.serde.ArrayLen is defined multiple times

Hi, I encountered the same problem as #109 . When I was building the release package, it kept failing:
Type com.novi.serde.ArrayLen is defined multiple times: /Users/ontwang/.gradle/caches/transforms-3/0fce63b0b50a8e8efde10c93f35c117f/transformed/jetified-client-sdk-java-1.0.5.jar:com/novi/serde/ArrayLen.class, /Users/ontwang/Documents/xxx/xxxxx/ji_app/platform/android/bloc/build/intermediates/runtime_library_classes_jar/release/classes.jar:com/novi/serde/ArrayLen.class

I tried to get the latest example from crux master. After modifying the build.grale, it still didn’t work with same error. And My global dependencies were also removed from diem. These attempts took me a whole day. I want to know why I still get this error when novi is no longer in my dependency tree.

android {
    namespace 'com.minij.app.bloc'
    compileSdk 33
    ndkVersion '25.2.9519653'
    defaultConfig {
        minSdk 26
        targetSdk 33

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles "consumer-rules.pro"
    }

    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    sourceSets {
        main.java.srcDirs += "${projectDir}/../../../models/generated/java/com"
    }
}

dependencies {

    implementation 'androidx.core:core-ktx:1.8.0'
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.9.0'
    implementation "net.java.dev.jna:jna:5.12.1@aar"
    implementation('com.diem:client-sdk-java:1.0.5') {
        exclude group: 'org.bouncycastle', module: 'bcprov-jdk15to18'
        exclude group: 'com.novi.serde', module: 'ArrayLen'
    }

}

apply plugin: 'org.mozilla.rust-android-gradle.rust-android'

cargo {
    pythonCommand = "python3"
    module  = "../../.."
    libname = "bloc"
    targets = ["arm64"]
    extraCargoBuildArguments = ['--package', 'bloc']
}

afterEvaluate {
    // The `cargoBuild` task isn't available until after evaluation.
    android.libraryVariants.all { variant ->
        def productFlavor = ""
        variant.productFlavors.each {
            productFlavor += "${it.name.capitalize()}"
        }
        def buildType = "${variant.buildType.name.capitalize()}"

        tasks.named("compileDebugKotlin") {
            it.dependsOn(tasks.named("typesGen"), tasks.named("bindGen"))
        }

        tasks.named("generate${productFlavor}${buildType}Assets") {
//            it.dependsOn(tasks.named("cargoBuild"),tasks["createSymlink"])
            it.dependsOn(tasks.named("cargoBuild"))
        }

    }
}


tasks.register('bindGen', Exec) {
    def outDir = "${projectDir}/src/main/java"
    workingDir "../../../"

    commandLine(
                "sh", "-c",
                """\
                cargo build -p bloc && \
                target/debug/uniffi-bindgen generate bloc/src/bloc.udl \\
                --language kotlin \
                --out-dir $outDir
                """
        )
}

tasks.register('typesGen', Exec) {
    workingDir "../../"

    commandLine("sh", "-c", "cargo build -p models")

}

tasks.register( 'createSymlink',Exec) {
    def libblocPath = "bloc/build/rustJniLibs/android/arm64-v8a/libbloc.so"
    def jniLibsPath ="app/src/main/jniLibs/arm64-v8a/libbloc.so"
    workingDir "../"

    commandLine(
            'cp', '-f', libblocPath,jniLibsPath
    )
}

and i try to modify bindGen for cargo build -r bloc && \ , not work

and i try to delete anything of diem ,it take error :

Missing class com.google.common.collect.ArrayListMultimap (referenced from: java.lang.Object com.alibaba.fastjson.serializer.GuavaCodec.deserialze(com.alibaba.fastjson.parser.DefaultJSONParser, java.lang.reflect.Type, java.lang.Object))
Missing class com.google.common.collect.Multimap (referenced from: void com.alibaba.fastjson.serializer.GuavaCodec.write(com.alibaba.fastjson.serializer.JSONSerializer, java.lang.Object, java.lang.Object, java.lang.reflect.Type, int))
Missing class dalvik.system.VMStack (referenced from: void com.tencent.smtt.export.external.DexLoader.<init>(java.lang.String, android.content.Context, java.lang.String[], java.lang.String, java.util.Map))
Missing class java.awt.Color (referenced from: java.lang.Object com.alibaba.fastjson.serializer.AwtCodec.deserialze(com.alibaba.fastjson.parser.DefaultJSONParser, java.lang.reflect.Type, java.lang.Object) and 3 other contexts)
Missing class java.awt.Font (referenced from: java.lang.Object com.alibaba.fastjson.serializer.AwtCodec.deserialze(com.alibaba.fastjson.parser.DefaultJSONParser, java.lang.reflect.Type, java.lang.Object) and 3 other contexts)
Missing class java.awt.Point (referenced from: java.lang.Object com.alibaba.fastjson.serializer.AwtCodec.deserialze(com.alibaba.fastjson.parser.DefaultJSONParser, java.lang.reflect.Type, java.lang.Object) and 3 other contexts)

and i try to add

packagingOptions {
exclude 'com/novi/serde/*'
}

still not work ....

Looking forward reply, thanks

Example iOS configuration not valid for TestFlight / deployment

I recently got an Crux iOS app into TestFlight and noticed that our example configurations need a few tweaks to pass the app validation stage

  1. uniffi-bingen binary needs to be excluded from sources. With xcodegen that looks as follows
    settings:
      OTHER_LDFLAGS: [-w]
      SWIFT_OBJC_BRIDGING_HEADER: generated/sharedFFI.h
      ENABLE_USER_SCRIPT_SANDBOXING: NO
      "EXCLUDED_SOURCE_FILE_NAMES[arch=*]": "uniffi-bindgen"
  2. In the build script which generates the FFI, we skip the step in an indexbuild phase, but we also need to skip it in the install buildphase, because the paths are all different, so the build fails, and it shouldn't run anyway
  3. Step 1 might cover this, not sure, but we should generated the shared Xcode project with cargo xcode --skip-install, so that any binaries don't get bundled in the deployment archive (which makes it invalid as an app archive).

Perf benchmark the bridge across platforms

Out of curiosity and also to aleviate any concerns, we should performance test the bridge messaging to see how many messages can we pass back and forth as a base line

Add an example using vite as a JS/TS build tool

For a few reasons it was a bit harder than expected adapting the examples to work with Vite (+solid-js): I hit a few problems which weren't obvious to troubleshoot.

I'm planning on adding a web target to the counter example, using Vite and SolidJs.

Allow for an atomic define-able, composable global store

Hey Crux team, after understanding a bit more about how Crux works and how the state management is defined, I can see history repeating itself. 1 massive key value pair serializable global store will not scale well. It needs to be distributed. For example, the event handler loop can be broken from a flat enum switch to a nested enum switch, where the first word is the first enum, and the second word is the second enum, and that determines how an event is handled in a nested fashion, allowing the app to scale.

That model doesn't work for state, and in js-land where we've gone through all these stages of the global state model, what we've arrived at as the best solution for scaling an app is an atomic store, like jotai. It would be really cool if Crux could support or let us opt in to coding that way instead of a massive key value pair store, that may not scale well.

Xcode fails to inherit `PATH` and therefore fails to find nix-installed cargo

I've successfully run cargo build --package shared && cargo build --package shared_types and opened the project in xcode.

In Xcode, attempting to build gives multiple errors, the first of which is:

RuleScriptExecution /Users/n8henrie/git/crux/examples/counter/iOS/generated/shared.swift /Users/n8henrie/git/crux/examples/counter/iOS/generated/sharedFFI.h /Users/n8henrie/git/crux/examples/counter/shared/src/shared.udl normal undefined_arch (in target 'CounterApp' from project 'CounterApp')
    cd /Users/n8henrie/git/crux/examples/counter/iOS
    /bin/sh -c \#\!/bin/bash'
'set\ -e'
''
'\#\ Skip\ during\ indexing\ phase\ in\ XCode\ 13+'
'if\ \[\ \"\$ACTION\"\ \=\=\ \"indexbuild\"\ \]\;\ then'
'\ \ echo\ \"Not\ building\ \*.udl\ files\ during\ indexing.\"'
'\ \ exit\ 0'
'fi'
''
'\#\ Skip\ for\ preview\ builds'
'if\ \[\ \"\$ENABLE_PREVIEWS\"\ \=\ \"YES\"\ \]\;\ then'
'\ \ echo\ \"Not\ building\ \*.udl\ files\ during\ preview\ builds.\"'
'\ \ exit\ 0'
'fi'
''
'cd\ \"\$\{INPUT_FILE_DIR\}/..\"'
'\"\$\{BUILD_DIR\}/debug/uniffi-bindgen\"\ generate\ \"src/\$\{INPUT_FILE_NAME\}\"\ --language\ swift\ --out-dir\ \"\$\{PROJECT_DIR\}/generated\"'
'

/bin/sh: line 16: /Users/n8henrie/Library/Developer/Xcode/DerivedData/CounterApp-fwwrdheqxdkprxbqjomnsznablpm/Build/Products/debug/uniffi-bindgen: No such file or directory
Command RuleScriptExecution failed with a nonzero exit code

Looking through a few other errors, I suspect this may be because I'm using a nix-installed cargo:

$ type -p cargo
/run/current-system/sw/bin/cargo

If the xcode build scripts are trying to call cargo commands, my recollection is that xcode won't necessarily have cargo in its path; some people also search for a rustup-installed cargo at ~/.cargo/bin or a brew-installed cargo at /opt/homebrew/bin (or /usr/local), but there are other possibilities.

Maybe related: #83

Typegen fails when a Uuid is included

I was trying to pass a Uuid value in an Event variant, but this breaks typegen. It seems like the typegen tries to create an "empty" Uuid, which doesn't work.

I've created a minimal reproduction in the counter example app in this commit on this branch. I don't think it matters where the Uuid appears in the types registered with typegen.

Any ideas about how to tweak the typegen to handle Uuids?

Effect testing ergonomic improvements

It would be good to provide nicer testing API for effects. AppTester goes quite far in making this convenient, but there are a few tasks that come up a lot in tests that are still a bit cumbersome:

  • Asserting that a specific kind of effect was requested
  • Finding a request for a particular effect, and returning it (failing if none is present)
  • Filtering requested effects down to a particular effect type(s)

@obmarg suggested a nice solution for all that by implementing TryInto<[operation type]> on Effect in the effect derive macro, allowing convenient conversions between Effect and the expected operation with error handling.

Adding Compose capability caused typegen to fail

I tried adding the Compose capability to my app. The shared crate compiles, but typegen fails in shared_types, because the Never enum has no cases. See the backtrace below for details.

We don't need to include Compose or Never in typegen, since they are not exposed to shells, but is there a way to mark a capability as "excluded"? Or maybe another workaround?

Here's a minimal repo that produces the error. You don't even have to use Compose, just include it in Capabilities.

❯ RUST_BACKTRACE=1 RUSTFLAGS=-Awarnings cargo build -p shared_types
   Compiling shared_types v0.1.0 (/Users/eric/code/crux_compose/shared_types)
error: failed to run custom build command for `shared_types v0.1.0 (/Users/eric/code/crux_compose/shared_types)`
note: To improve backtraces for build dependencies, set the CARGO_PROFILE_DEV_BUILD_OVERRIDE_DEBUG=true environment variable to enable debug information generation.

Caused by:
  process didn't exit successfully: `/Users/eric/code/crux_compose/target/debug/build/shared_types-5b2877aeefdefaf8/build-script-build` (exit status: 101)
  --- stdout
  cargo:rerun-if-changed=../shared

  --- stderr
  thread 'main' panicked at /Users/eric/.cargo/registry/src/index.crates.io-6f17d22bba15001f/serde-reflection-0.3.6/src/de.rs:431:18:
  variant indexes must be a non-empty range 0..variants.len()
  stack backtrace:
     0: rust_begin_unwind
               at /rustc/7cf61ebde7b22796c69757901dd346d0fe70bd97/library/std/src/panicking.rs:647:5
     1: core::panicking::panic_fmt
               at /rustc/7cf61ebde7b22796c69757901dd346d0fe70bd97/library/core/src/panicking.rs:72:14
     2: core::panicking::panic_display
               at /rustc/7cf61ebde7b22796c69757901dd346d0fe70bd97/library/core/src/panicking.rs:196:5
     3: core::panicking::panic_str
               at /rustc/7cf61ebde7b22796c69757901dd346d0fe70bd97/library/core/src/panicking.rs:171:5
     4: core::option::expect_failed
               at /rustc/7cf61ebde7b22796c69757901dd346d0fe70bd97/library/core/src/option.rs:1988:5
     5: core::option::Option<T>::expect
     6: <serde_reflection::de::Deserializer as serde::de::Deserializer>::deserialize_enum::{{closure}}
     7: alloc::collections::btree::map::entry::Entry<K,V,A>::or_insert_with
     8: <serde_reflection::de::Deserializer as serde::de::Deserializer>::deserialize_enum
     9: crux_core::capability::_::<impl serde::de::Deserialize for crux_core::capability::Never>::deserialize
    10: serde_reflection::trace::Tracer::trace_type_once
    11: serde_reflection::trace::Tracer::trace_type
    12: serde_reflection::trace::Tracer::trace_simple_type
    13: crux_core::typegen::TypeGen::register_type
    14: <shared::app::Capabilities as crux_core::typegen::Export>::register_types
    15: crux_core::typegen::TypeGen::register_app
    16: build_script_build::main
    17: core::ops::function::FnOnce::call_once
  note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

Provide shell-side Core wrapper boilerplate for Swift, Kotlin and TypeScript

All the example share a very similar implemenetation of the core intereaction, servicing the messaging and keepting the shell-side view model up to date.

At the moment all the implementations couple the serialisation and view model management with other effect handling, but that can be improved.

We should design a common API surface for the Core and provide it as an "SDK" for each supported foreingn language.

A rough list of steps to achieve this:

  • Design a Core API which
    • Has update interface serialising the exported Events
    • Surfaces an "observable" (for the given UI toolkit) ViewModel instance, which is kept up to date
    • Services the Render effect and updates the view model
    • Allows for plugging in effect handlers for other effects (this probably requires a few POCs)
  • include an implementation in the exported foreign language code
  • move the doctor templates and examples to use the API

RFC: Testing convenience API

This is a list of nice to have APIs on the AppTester to make tests a little bit verbose in common cases. I'll be updating this issue as I think of them. We can also discuss them as we go, and when there's a few of them, I'll open a PR implementing them.

  • tester.resolve_and_update - resolves an effect and immediately sends any resulting events

  • update.filter_effects(Effect::into_x) - shorthand for update.into_effects().filter_map(Effect::into_x)

  • update.first_effect(Effect::into_x) - shorthand for update.into_effects().filter_map(Effect::into_x).next().ok_or(some_error)

  • macro combining the effect finding shorthand with assert_let like behaviour on the operation the effect carries, if possible. It's quite common to pull data out of the requests, while also holding onto the request in order to resolve it.

    E.g. something like

    let mut request = assert_first_effect_let!(Effect::into_x, XOperation { field: thing });
    tester.resolve(&mut request, format!("Received: {}", thing))

Getting started failing to compile when asked to run `cargo build`

Unsure if this is intentional but running cargo build at this point

will fail with the bellow, which may confuse new users

error: failed to load manifest for workspace member `/shared_types`

Caused by:
  failed to read `/shared_types/Cargo.toml`

Caused by:
  No such file or directory (os error 2)

removing the additional members from here obviously breaks the example but will allow cargo build to pass at this point

Android shared_types can have stale/obsolete files

For discussion: I expect the shared_types directory in my Android project to always exactly match the shared_types/generated/java directory.

As I've been refactoring my app, though, I've noticed that once a generated file is copied into the shared_types directory in my Android project, it never goes away, even if the corresponding file in shared_types/generated/java goes away, because the "standard" Android task never deletes anything.

I know this is a project-specific build task, but I wonder if it makes sense to update the suggested snippet. If I modify the Android typesGen task like this, I get my expected results. It might not be best to use rsync for the general case, but this is just an illustration of how I expect the task to work (I had to be more specific about the paths or this task would also delete shared/shared.kt).

task typesGen(type: Exec) {
    def noviPackagePath = "com/novi"
    def sharedTypesPackagePath = "com/path/to/my/shared_types"
    def outDir = "${projectDir}/src/main/java"
    def srcDir = "shared_types/generated/java"
    workingDir "../../"
    commandLine(
            "sh", "-c",
            """\
            rsync -av --delete $srcDir/$noviPackagePath/ $outDir/$noviPackagePath/ && \
            rsync -av --delete $srcDir/$sharedTypesPackagePath/ $outDir/$sharedTypesPackagePath/ \
            # cp -r $srcDir $outDir <-- the current script
            """
    )
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.