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

WASM support for client #388

Closed
wants to merge 3 commits into from
Closed

Conversation

whizsid
Copy link

@whizsid whizsid commented Jan 7, 2023

Separated client and server code by introducing new features client, server. Because we have to minify the generated WASM binary.

I used wasm-timer as a replacement for tokio::time. But wasm-timer seems like dead today. I had to copy lot of code from tokio_util::time to implement the delay_queue for WASM. tomaka/wasm-timer#21 . Stole this example project from #331 and improved it.

@tikue
Copy link
Collaborator

tikue commented Jan 7, 2023

Hey, thanks for your contribution!

I think I don't want to maintain a timer in-tree. Are you willing to maintain the forked wasm timer? If so, I encourage you to publish it as a crate and add an optional dependency on it in tarpc.

@whizsid
Copy link
Author

whizsid commented Jan 8, 2023

@tikue Yes I like to maintain a one. I forked it just now. I will update this PR once I finalized the project.

@allsey87
Copy link

Any updates on this PR? I also just bumped into the tokio::time issue as a result of trying to get tarpc working in wasm: tarpc-web.log

@whizsid
Copy link
Author

whizsid commented Feb 12, 2023

@allsey87 I am still working on the new timer crate. I hope to release it in this week after implemented few more unit tests for DelayQueue. https://github.com/whizsid/wasmtimer-rs

@tekacs
Copy link

tekacs commented Feb 22, 2023

Not to add anything here, but I was able to very successfully use this PR to bind my WASM frontend crate to my backend crate over a quick (possibly incorrect 😄, so use at your own caution) Websocket transport that I chucked together:

https://gist.github.com/tekacs/b2cb7d19e8ec9a7c7838251b2e427d01

No issues so far, but will chime in if I notice anything -- happy to write this up as a proper end-to-end example once this is merged if I'm not doing things wrong or it's not likely to be gotten to.

Thanks so much for this @whizsid!

@tekacs
Copy link

tekacs commented Apr 20, 2023

Would love to see this PR merged (I use it regularly, it works well albeit with a fork to wasmtimer-rs that I might ask @whizsid to upstream) -- I'm happy to resolve conflicts on this branch if there's appetite from the maintainers to have this one merged?

@tikue
Copy link
Collaborator

tikue commented Apr 20, 2023

Hi, thanks for your interest! I think I just need a bit more information to make a decision on this:

  • It'd be nice if the tarpc code didn't have to cfg-switch on different timers. Why can't tokio-timer merge the changes in wasm-timer?
  • Barring that, I'd be a bit nervous to have tarpc take a depenency on a crate I'm not super familiar with. Is there a way tokio-timer could be made generic so that wasm-timer could reuse the core data structures?

I think maybe a good next step is to talk to the tokio folks to see what's going on with wasm support?

@tikue
Copy link
Collaborator

tikue commented Apr 20, 2023

It looks like tokio support for wasm is planned: tokio-rs/tokio#4827. Given that, I think I would prefer to not merge this and just wait for that support. Sorry about that! But please let me know if tokio's support does not pan out for some reason.

@tikue tikue closed this Apr 20, 2023
@tikue
Copy link
Collaborator

tikue commented Apr 20, 2023

Looking at this PR more closely, I see that it also would have split the client and server into separate features to minimize size for clients that don't need the server. I think that would be a great, uncontroversial, change and would be happy to merge it in a new PR!

@tekacs
Copy link

tekacs commented Apr 20, 2023

So @tikue that issue that you're linking to is (as far as I can tell) for WASI support for Tokio rather than general WASM support -- in particular this PR supports usage of TARPC in WASM in the browser, which that proposal wouldn't lead to support for.

Would that be reason enough to still consider this proposal at all?

It's not essential to my use to see this merged (I can maintain my own fork), but I just wanted to call out that Tokio/WASI isn't the full picture (and @whizsid's work with timers in particular helps especially in browsers).

@whizsid
Copy link
Author

whizsid commented Apr 20, 2023

@tekacs I opened this PR to use tarpc in a cross-platform (web and desktop) application. But now I am not using the tarpc.
Because the implementation of wasmtimer was done on a tricky way . So I can not use it in a production-ready application. If we look from the user's POV, they can do a simple timeout by calling the setTimeout once. But the wasmtimer is calling the setTimeout too frequently to do the same thing.

My personal idea on a wasm tarpc is, someone can make a new runtime agnostic framework by handing all the time related functionalities to users. Then this project can support both web and desktop.

As an alternative to tarpc, I am using below struct. I am posting it here to save someone who fell into the same well.

pub struct Client<
    I: Serialize + DeserializeOwned + Clone + Debug + Sized,
    O: Serialize + DeserializeOwned + Clone + Debug + Sized,
    E: Debug,
    T: Stream<Item = Vec<u8>> + Sink<Vec<u8>, Error = E> + Unpin,
> {
    internal: T,
    pending: Vec<I>,
    _out: PhantomData<O>,
}

impl<
        I: Serialize + DeserializeOwned + Clone + Debug + Sized,
        O: Serialize + DeserializeOwned + Clone + Debug + Sized,
        E: Debug,
        T: Stream<Item = Vec<u8>> + Sink<Vec<u8>, Error = E> + Unpin,
    > Client<I, O, E, T>
{
    pub fn new(internal: T) -> Client<I, O, E, T> {
        Client {
            internal,
            pending: Vec::new(),
            _out: PhantomData,
        }
    }

    /// Send a request and waiting for a response
    pub async fn send_and_receive<OT: Into<O>, IT: TryFrom<I, Error = ()>>(
        &mut self,
        message: OT,
    ) -> Result<IT, SendAndReceiveError<E>> {
        self.send(message).await?;
        let response = self.receive::<IT>().await?;
        Ok(response)
    }

    pub async fn receive<IT: TryFrom<I, Error = ()>>(&mut self) -> Result<IT, ReceiveError> {
        // Immediately returning if it in pending list
        for (i, pending_message) in self.pending.iter().enumerate() {
            if let Ok(converted_message) = IT::try_from(pending_message.clone()) {
                self.pending.remove(i);
                return Ok(converted_message);
            }
        }

        loop {
            let response_message_opt = self.internal.next().await;
            if let Some(bin_message) = response_message_opt {
                match from_bin::<I>(&bin_message) {
                    Ok(response_message) => {
                        if let Ok(converted_message) = IT::try_from(response_message.clone()) {
                            return Ok(converted_message);
                        } else {
                           // Inserting it to pending list to return it in next time.
                            self.pending.push(response_message);
                        }
                    }
                    Err(e) => {
                        return Err(ReceiveError::Deserialize(*e));
                    }
                }
            }
        }
    }

    /// Send a message without caring about a response
    pub async fn send<OT: Into<O>>(&mut self, message: OT) -> Result<(), SendError<E>> {
        let mut pin_internal: Pin<&mut T> = Pin::new(&mut self.internal);
        match to_bin(&message.into()) {
            Ok(serialized) => pin_internal
                .send(serialized)
                .await
                .map_err(|e| SendError::Send(e)),
            Err(e) => Err(SendError::Serialize(*e)),
        }
    }
}

#[derive(Debug)]
pub enum SendError<E: Debug> {
    Serialize(BincodeError),
    Send(E),
}

#[derive(Debug)]
pub enum ReceiveError {
    Deserialize(BincodeError),
}

#[derive(Debug)]
pub enum SendAndReceiveError<E: Debug> {
    Send(SendError<E>),
    Receive(ReceiveError),
}

impl<E: Debug> From<SendError<E>> for SendAndReceiveError<E> {
    fn from(value: SendError<E>) -> Self {
        SendAndReceiveError::Send(value)
    }
}

impl<E: Debug> From<ReceiveError> for SendAndReceiveError<E> {
    fn from(value: ReceiveError) -> Self {
        SendAndReceiveError::Receive(value)
    }
}

This can be improved. But now I can use it like this.

// on web frontend
let client:Client<ServerMessage, ApplicationMessage, WebSocketError, WebSocket> = Client::new(websocket_transport);
// on standalone application UI thread
// I created this bi_channel by combining two futures channels
let client:Client<ServerMessage, ApplicationMessage, BiChannelError, BiChannel> = Client::new(bi_channel_1);
// on server
let client:Client<ApplicationMessage, ServerMessage, WebSocketError, WebSocket> = Client::new(web_socket);
// on standalone application main thread
let client:Client<ApplicationMessage, ServerMessage, BiChannelError, BiChannel> = Client::new(bi_channel_2);

Then I can send and receive messages on any platform like:-

client.send(MyMessage::new()).await;
let other_message = client.receive::<OtherMessage>().await;

This is how I achieved it:- https://github.com/whizsid/openxd

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants