├── .gitignore ├── Cargo.toml ├── README.md ├── src └── lib.rs ├── webchat-client-rs ├── Cargo.toml ├── Makefile ├── src │ └── lib.rs └── web │ ├── index.html │ ├── index.js │ ├── package.json │ ├── webchat_client.js │ └── webpack.config.js └── webchat-server-rs ├── Cargo.toml └── src └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target/ 3 | **/*.rs.bk 4 | Cargo.lock 5 | **/*.log 6 | **/target/ 7 | /**/node_modules/ 8 | **/*.wasm 9 | **/webchat_client_rs.js 10 | **/*.lock -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "webchat-rs" 3 | version = "0.1.0" 4 | authors = ["Teemu Erkkola "] 5 | 6 | [dependencies] 7 | serde = "1.0.37" 8 | serde_derive = "1.0.37" 9 | bincode = "1" 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webchat-client-rs 2 | 3 | A doodle to create a mostly-rust client-server web app with a shared data model and binary communication over websocket between a server and WebAssembly client built using wasm-bindgen. 4 | 5 | ## Instructions 6 | 7 | To build and run the server, run `cargo run localhost:8081` inside the `webchat-server-rs` directory. 8 | 9 | The client requires nightly toolchain, wasm32-unknown-unknown target and wasm-bindgen tool: `rustup install nightly && rustup target add wasm32-unknown-unknown --toolchain nightly && cargo install wasm-bindgen-cli` 10 | 11 | To build and run the client first run `npm install` inside `webchat-client-rs/web`, then run `make run` inside the `webchat-client-rs` directory. It will run at `localhost:8080`. 12 | 13 | ## Known issues 14 | 15 | Does not currently work in chromium. Explained in [wasm-bindgen's chrome caveat](https://github.com/rustwasm/wasm-bindgen/blob/master/examples/hello_world/README.md#caveat-for-chrome-users). 16 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate serde; 2 | #[macro_use] 3 | extern crate serde_derive; 4 | extern crate bincode; 5 | 6 | use bincode::{deserialize as bin_de, serialize as bin_ser, Error}; 7 | 8 | #[derive(Serialize, Deserialize,Debug,PartialEq)] 9 | pub enum Message { 10 | Ping, Pong, Chat(String), Nick(String), Me(String) 11 | } 12 | 13 | pub fn serialize(message: Message) -> Result, Error> { 14 | bin_ser(&message) 15 | } 16 | 17 | pub fn deserialize(buffer: &[u8]) -> Result { 18 | bin_de(buffer) 19 | } 20 | 21 | 22 | #[cfg(test)] 23 | mod test { 24 | use ::*; 25 | #[test] 26 | fn serde_ping() { 27 | let ping = Message::Ping; 28 | let serialized = serialize(ping).unwrap(); 29 | let deserialized = deserialize(&serialized).unwrap(); 30 | assert!(deserialized == Message::Ping); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /webchat-client-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "webchat-client-rs" 3 | version = "0.1.0" 4 | authors = ["Teemu Erkkola "] 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | [dependencies] 10 | wasm-bindgen = "0.2" 11 | webchat-rs = { path = ".." } 12 | 13 | [profile.release] 14 | opt-level = 'z' 15 | lto = true 16 | panic = 'abort' 17 | 18 | -------------------------------------------------------------------------------- /webchat-client-rs/Makefile: -------------------------------------------------------------------------------- 1 | WASM_BINDGEN=~/.cargo/bin/wasm-bindgen 2 | MODE=release 3 | 4 | default: run 5 | 6 | run: web/webchat_client_rs_bg.wasm 7 | cd web; npm run serve; 8 | 9 | web/webchat_client_rs_bg.wasm: target/wasm32-unknown-unknown/${MODE}/webchat_client_rs.wasm 10 | ${WASM_BINDGEN} target/wasm32-unknown-unknown/${MODE}/webchat_client_rs.wasm --out-dir web 11 | 12 | target/wasm32-unknown-unknown/${MODE}/webchat_client_rs.wasm: src/lib.rs 13 | cargo +nightly build --target wasm32-unknown-unknown --${MODE} 14 | -------------------------------------------------------------------------------- /webchat-client-rs/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(proc_macro, wasm_custom_section, wasm_import_module)] 2 | 3 | extern crate wasm_bindgen; 4 | extern crate webchat_rs; 5 | 6 | use wasm_bindgen::prelude::*; 7 | use webchat_rs::*; 8 | 9 | #[wasm_bindgen(module = "./webchat_client")] 10 | extern { 11 | fn send(msg: &[u8]); 12 | #[wasm_bindgen(js_name = addMessage)] 13 | fn add_message(msg: &str); 14 | } 15 | 16 | #[wasm_bindgen] 17 | extern { 18 | #[wasm_bindgen(js_namespace = console)] 19 | fn log(s: &str); 20 | } 21 | 22 | fn send_message(msg: Message) { 23 | // Message serialization should ALWAYS succeed 24 | send(&serialize(msg).unwrap()); 25 | } 26 | #[wasm_bindgen] 27 | pub fn main() { 28 | send_message(Message::Ping); 29 | send_message(Message::Chat("Hello World!".to_owned())); 30 | } 31 | 32 | #[wasm_bindgen] 33 | pub fn recv(buffer: &[u8]) { 34 | if let Ok(msg) = deserialize(buffer) { 35 | match msg { 36 | Message::Ping => send_message(Message::Pong), 37 | Message::Pong => log("got Pong!"), 38 | Message::Chat(content) | Message::Me(content) => add_message(&content), 39 | Message::Nick(nick) => { 40 | let mut msg = "You are now known as ".to_owned(); 41 | msg.push_str(&nick); 42 | add_message(&msg); 43 | } 44 | }; 45 | } else { 46 | log("Received invalid message!"); 47 | } 48 | } 49 | 50 | #[wasm_bindgen] 51 | pub fn input(msg: &str) { 52 | if msg.starts_with("/nick") { 53 | if let Some(nick) = msg.splitn(2, " ").skip(1).next() { 54 | send_message(Message::Nick(nick.to_owned())); 55 | } else { 56 | add_message("Usage: /nick "); 57 | } 58 | } else if msg.starts_with("/me") { 59 | if let Some(content) = msg.splitn(2, " ").skip(1).next() { 60 | send_message(Message::Me(content.to_owned())); 61 | } else { 62 | add_message("Usage: /me "); 63 | } 64 | } else if msg.starts_with("/") { 65 | add_message("Unknown command") 66 | } else { 67 | send_message(Message::Chat(msg.to_owned())); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /webchat-client-rs/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | webchat-client-rs 6 | 7 | 52 | 53 | 54 |

webchat-client-rs

55 |
    56 |
    57 | 58 | 59 |
    60 | 61 | 62 | -------------------------------------------------------------------------------- /webchat-client-rs/web/index.js: -------------------------------------------------------------------------------- 1 | import { run } from "./webchat_client"; 2 | import("./webchat_client_rs").then(run); 3 | -------------------------------------------------------------------------------- /webchat-client-rs/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "serve": "webpack-dev-server" 4 | }, 5 | "devDependencies": { 6 | "webpack": "^4.0.1", 7 | "webpack-cli": "^2.0.10", 8 | "webpack-dev-server": "^3.1.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /webchat-client-rs/web/webchat_client.js: -------------------------------------------------------------------------------- 1 | let app = null; 2 | let socket = null; 3 | 4 | export function run(currentApp) { 5 | app = currentApp; 6 | socket = new WebSocket('ws://127.0.0.1:8081'); 7 | socket.binaryType = 'arraybuffer'; 8 | socket.addEventListener('message', e => app.recv(new Uint8Array(e.data))); 9 | socket.addEventListener('open', e => app.main()); 10 | 11 | document.getElementById("messageForm").addEventListener('submit', e => { 12 | e.preventDefault(); 13 | let messageInput = document.getElementById("messageInput") 14 | app.input(messageInput.value); 15 | messageInput.value = ""; 16 | }); 17 | } 18 | 19 | export function send(msg) { 20 | socket.send(msg); 21 | } 22 | 23 | export function addMessage(msg) { 24 | let messages = document.getElementById('messages'); 25 | let messageItem = document.createElement("li"); 26 | messageItem.textContent = msg; 27 | messages.appendChild(messageItem); 28 | messages.scrollTop = messages.scrollTopMax; 29 | } 30 | -------------------------------------------------------------------------------- /webchat-client-rs/web/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: "./index.js", 5 | output: { 6 | path: path.resolve(__dirname, "dist"), 7 | filename: "index.js", 8 | }, 9 | mode: "development" 10 | }; 11 | -------------------------------------------------------------------------------- /webchat-server-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "webchat-server-rs" 3 | version = "0.1.0" 4 | authors = ["Teemu Erkkola "] 5 | 6 | [dependencies] 7 | ws = "0.7.6" 8 | webchat-rs = { path = ".." } 9 | -------------------------------------------------------------------------------- /webchat-server-rs/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate webchat_rs; 2 | extern crate ws; 3 | 4 | use ws::listen; 5 | use webchat_rs::*; 6 | use std::cell::RefCell; 7 | use std::env; 8 | 9 | struct WsMsg(Message); 10 | impl Into for WsMsg { 11 | fn into(self) -> ws::Message { 12 | // Message serialization should ALWAYS succeed 13 | ws::Message::Binary(serialize(self.0).unwrap()) 14 | } 15 | } 16 | 17 | fn main() { 18 | let args: Vec<_> = env::args().collect(); 19 | 20 | if args.len() != 2 { 21 | println!("Usage: {} ", args[0]); 22 | return; 23 | } 24 | 25 | let server = listen(&args[1], |out| { 26 | match out.send(WsMsg(Message::Ping)) { 27 | Ok(_) => println!("Ping?"), 28 | Err(e) => eprintln!("Error sending initial ping: {:?}", e) 29 | }; 30 | 31 | let nick = RefCell::new("Anonymous".to_owned()); 32 | 33 | move |msg| { 34 | if let ws::Message::Binary(buffer) = msg { 35 | if let Ok(msg) = deserialize(&buffer) { 36 | match msg { 37 | Message::Ping => { 38 | out.send(WsMsg(Message::Pong)) 39 | }, 40 | Message::Pong => { println!("Pong!"); Ok(()) } 41 | Message::Chat(content) => { 42 | let msg = Message::Chat(format!("{}: {}", nick.borrow(), content)); 43 | out.broadcast(WsMsg(msg)) 44 | }, 45 | Message::Nick(value) => { 46 | nick.replace(value.clone()); 47 | out.send(WsMsg(Message::Nick(value))) 48 | }, 49 | Message::Me(content) => { 50 | let msg = Message::Me(format!("{} {}", nick.borrow(), content)); 51 | out.broadcast(WsMsg(msg)) 52 | }, 53 | } 54 | } else { 55 | Err(ws::Error::new(ws::ErrorKind::Internal, "Deserialization error")) 56 | } 57 | } else { 58 | Err(ws::Error::new(ws::ErrorKind::Internal, "Not a binary message")) 59 | } 60 | } 61 | }); 62 | 63 | match server { 64 | Ok(_) => println!("Exited successfully"), 65 | Err(e) => eprintln!("Server error: {:?}", e) 66 | }; 67 | } 68 | --------------------------------------------------------------------------------