├── .gitignore ├── Cargo.toml ├── README.md ├── examples ├── simple.rs ├── test.rs ├── typed_elements.js ├── typed_elements.rs └── wry.rs ├── sledgehammer_bindgen_macro ├── Cargo.toml └── src │ ├── builder.rs │ ├── encoder.rs │ ├── function.rs │ ├── lib.rs │ └── types │ ├── mod.rs │ ├── numbers.rs │ ├── slice.rs │ ├── string.rs │ └── writable.rs └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sledgehammer_bindgen" 3 | version = "0.6.0" 4 | authors = ["Evan Almloff "] 5 | edition = "2021" 6 | description = "Fast batched js bindings" 7 | documentation = "https://docs.rs/sledgehammer_bindgen" 8 | readme = "README.md" 9 | repository = "https://github.com/demonthos/sledgehammer_bindgen/" 10 | license = "MIT" 11 | keywords = ["web", "wasm", "dom"] 12 | categories = ["web-programming", "wasm", "api-bindings"] 13 | 14 | [dependencies] 15 | sledgehammer_bindgen_macro = { path = "sledgehammer_bindgen_macro", version = "0.6.0" } 16 | wasm-bindgen = { version = "0.2", optional = true } 17 | 18 | [dev-dependencies] 19 | rand = "0.8.5" 20 | getrandom = { version = "0.2", features = ["js"] } 21 | sledgehammer_utils = "0.3.0" 22 | wasm-bindgen = "0.2" 23 | wry = { version = "0.33.0", features = ["devtools"] } 24 | 25 | [dev-dependencies.web-sys] 26 | version = "0.3" 27 | features = ["Node", "console"] 28 | 29 | [features] 30 | default = [] 31 | web = ["wasm-bindgen", "sledgehammer_bindgen_macro/web"] 32 | 33 | [workspace] 34 | members = ["sledgehammer_bindgen_macro"] 35 | 36 | [[example]] 37 | name = "typed_elements" 38 | required-features = ["web"] 39 | 40 | [[example]] 41 | name = "test" 42 | required-features = ["web"] 43 | 44 | [[example]] 45 | name = "simple" 46 | required-features = ["web"] 47 | 48 | [[example]] 49 | name = "wry" 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

sledgehammer bindgen

3 |
4 |
5 | 6 | 7 | Crates.io version 9 | 10 | 11 | 12 | Download 14 | 15 | 16 | 17 | docs.rs docs 19 | 20 |
21 | 22 | # What is Sledgehammer Bindgen? 23 | Sledgehammer bindgen provides faster rust batched bindings for js code. 24 | 25 | # How does this compare to wasm-bindgen: 26 | - wasm-bindgen is a lot more general it allows returning values and passing around a lot more different types of values. For most users wasm-bindgen is a better choice. Sledgehammer is specifically designed for web frameworks that want low-level, fast access to the dom. 27 | 28 | - You can use sledgehammer bindgen with wasm-bindgen. See the docs and examples for more information. 29 | 30 | # Why is it fast? 31 | 32 | ## String decoding 33 | 34 | - Strings are expensive to decode, but the cost doesn't change much with the size of the string. Wasm-bindgen calls TextDecoder.decode for every string. Sledgehammer only calls TextEncoder.decode once per batch. 35 | 36 | - If the string is small, it is faster to decode the string in javascript to avoid the constant overhead of TextDecoder.decode 37 | 38 | - See this benchmark: https://jsbench.me/4vl97c05lb/5 39 | 40 | ## String Caching 41 | 42 | - You can cache strings in javascript to avoid decoding the same string multiple times. 43 | - If the string is static sledgehammer will hash by pointer instead of by value. 44 | 45 | ## Byte encoded operations 46 | 47 | - In sledgehammer every operation is encoded as a sequence of bytes packed into an array. Every operation takes 1 byte plus whatever data is required for it. 48 | 49 | - Each operation is encoded in a batch of four as a u32. Getting a number from an array buffer has a high constant cost, but getting a u32 instead of a u8 is not more expensive. Sledgehammer bindgen reads the u32 and then splits it into the 4 individual bytes. It will shuffle and pack the bytes into as few buckets as possible and try to inline reads into js. 50 | 51 | - See this benchmark: https://jsbench.me/csl9lfauwi/2 52 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | use sledgehammer_bindgen::bindgen; 2 | 3 | fn main() { 4 | #[bindgen] 5 | mod js { 6 | struct Channel; 7 | 8 | fn num(u1: u8, u2: u16, u3: u32) { 9 | "console.log($u1$, $u2$, $u3$);" 10 | } 11 | } 12 | 13 | let mut channel1 = Channel::default(); 14 | channel1.num(0, 0, 0); 15 | channel1.flush(); 16 | } 17 | -------------------------------------------------------------------------------- /examples/test.rs: -------------------------------------------------------------------------------- 1 | use sledgehammer_bindgen::bindgen; 2 | use wasm_bindgen::prelude::wasm_bindgen; 3 | use web_sys::{console, Node}; 4 | 5 | #[wasm_bindgen(inline_js = r#" 6 | class NodeInterpreter { 7 | constructor(){ 8 | this.nodes = [document.getElementById("main")]; 9 | } 10 | 11 | export function get_node(id){ 12 | return this.nodes[id]; 13 | } 14 | } 15 | "#)] 16 | extern "C" { 17 | #[wasm_bindgen] 18 | pub type NodeInterpreter; 19 | 20 | #[wasm_bindgen(method)] 21 | fn get_node(this: &NodeInterpreter, id: u16) -> Node; 22 | } 23 | 24 | fn main() { 25 | #[bindgen] 26 | mod js { 27 | #[extends(NodeInterpreter)] 28 | struct Channel; 29 | 30 | fn create_element(id: u16, name: &'static str) { 31 | "this.nodes[$id$]=document.createElement($name$);" 32 | } 33 | 34 | fn create_element_ns( 35 | id: u16, 36 | name: &'static str, 37 | ns: &'static str, 38 | ) { 39 | "this.nodes[$id$]=document.createElementNS($ns$,$name$);" 40 | } 41 | 42 | fn set_attribute(id: u16, name: &'static str, val: impl Writable) { 43 | "this.nodes[$id$].setAttribute($name$,$val$);" 44 | } 45 | 46 | fn remove_attribute(id: u16, name: &'static str) { 47 | "this.nodes[$id$].removeAttribute($name$);" 48 | } 49 | 50 | fn append_child(id: u16, id2: u16) { 51 | "this.nodes[$id$].appendChild(nodes[$id2$]);" 52 | } 53 | 54 | fn insert_before(parent: u16, id: u16, id2: u16) { 55 | "this.nodes[$parent$].insertBefore(nodes[$id$],nodes[$id2$]);" 56 | } 57 | 58 | fn set_text(id: u16, text: impl Writable) { 59 | "this.nodes[$id$].textContent=$text$;" 60 | } 61 | 62 | fn remove(id: u16) { 63 | "this.nodes[$id$].remove();" 64 | } 65 | 66 | fn replace(id: u16, id2: u16) { 67 | "this.nodes[$id$].replaceWith(this.nodes[$id2$]);" 68 | } 69 | 70 | fn clone(id: u16, id2: u16) { 71 | "this.nodes[$id2$]=this.nodes[$id$].cloneNode(true);" 72 | } 73 | 74 | fn first_child(id: u16) { 75 | "this.node[id]=this.node[id].firstChild;" 76 | } 77 | 78 | fn next_sibling(id: u16) { 79 | "this.node[id]=this.node[id].nextSibling;" 80 | } 81 | } 82 | 83 | let mut channel1 = Channel::default(); 84 | let main = 0; 85 | let node1 = 1; 86 | let node2 = 2; 87 | channel1.create_element(node1, "div"); 88 | channel1.create_element(node2, "span"); 89 | channel1.append_child(node1, node2); 90 | channel1.set_text(node2, "Hello World!"); 91 | channel1.append_child(main, node1); 92 | channel1.flush(); 93 | 94 | let typed: &NodeInterpreter = channel1.js_channel().as_ref(); 95 | console::log_1(&typed.get_node(0).into()); 96 | } 97 | -------------------------------------------------------------------------------- /examples/typed_elements.js: -------------------------------------------------------------------------------- 1 | class TypedElements { 2 | constructor() { 3 | this.nodes = [document.getElementById("main")]; 4 | this.els = [ 5 | "a", 6 | "abbr", 7 | "acronym", 8 | "address", 9 | "applet", 10 | "area", 11 | "article", 12 | "aside", 13 | "audio", 14 | "b", 15 | "base", 16 | "bdi", 17 | "bdo", 18 | "bgsound", 19 | "big", 20 | "blink", 21 | "blockquote", 22 | "body", 23 | "br", 24 | "button", 25 | "canvas", 26 | "caption", 27 | "center", 28 | "cite", 29 | "code", 30 | "col", 31 | "colgroup", 32 | "content", 33 | "data", 34 | "datalist", 35 | "dd", 36 | "del", 37 | "details", 38 | "dfn", 39 | "dialog", 40 | "dir", 41 | "div", 42 | "dl", 43 | "dt", 44 | "em", 45 | "embed", 46 | "fieldset", 47 | "figcaption", 48 | "figure", 49 | "font", 50 | "footer", 51 | "form", 52 | "frame", 53 | "frameset", 54 | "h1", 55 | "head", 56 | "header", 57 | "hgroup", 58 | "hr", 59 | "html", 60 | "i", 61 | "iframe", 62 | "image", 63 | "img", 64 | "input", 65 | "ins", 66 | "kbd", 67 | "keygen", 68 | "label", 69 | "legend", 70 | "li", 71 | "link", 72 | "main", 73 | "map", 74 | "mark", 75 | "marquee", 76 | "menu", 77 | "menuitem", 78 | "meta", 79 | "meter", 80 | "nav", 81 | "nobr", 82 | "noembed", 83 | "noframes", 84 | "noscript", 85 | "object", 86 | "ol", 87 | "optgroup", 88 | "option", 89 | "output", 90 | "p", 91 | "param", 92 | "picture", 93 | "plaintext", 94 | "portal", 95 | "pre", 96 | "progress", 97 | "q", 98 | "rb", 99 | "rp", 100 | "rt", 101 | "rtc", 102 | "ruby", 103 | "s", 104 | "samp", 105 | "script", 106 | "section", 107 | "select", 108 | "shadow", 109 | "slot", 110 | "small", 111 | "source", 112 | "spacer", 113 | "span", 114 | "strike", 115 | "strong", 116 | "style", 117 | "sub", 118 | "summary", 119 | "sup", 120 | "table", 121 | "tbody", 122 | "td", 123 | "template", 124 | "textarea", 125 | "tfoot", 126 | "th", 127 | "thead", 128 | "time", 129 | "title", 130 | "tr", 131 | "track", 132 | "tt", 133 | "u", 134 | "ul", 135 | "var", 136 | "video", 137 | "wbr", 138 | "xmp", 139 | ]; 140 | 141 | this.attrs = [ 142 | "accept-charset", 143 | "accept", 144 | "accesskey", 145 | "action", 146 | "align", 147 | "allow", 148 | "alt", 149 | "aria-atomic", 150 | "aria-busy", 151 | "aria-controls", 152 | "aria-current", 153 | "aria-describedby", 154 | "aria-description", 155 | "aria-details", 156 | "aria-disabled", 157 | "aria-dropeffect", 158 | "aria-errormessage", 159 | "aria-flowto", 160 | "aria-grabbed", 161 | "aria-haspopup", 162 | "aria-hidden", 163 | "aria-invalid", 164 | "aria-keyshortcuts", 165 | "aria-label", 166 | "aria-labelledby", 167 | "aria-live", 168 | "aria-owns", 169 | "aria-relevant", 170 | "aria-roledescription", 171 | "async", 172 | "autocapitalize", 173 | "autocomplete", 174 | "autofocus", 175 | "autoplay", 176 | "background", 177 | "bgcolor", 178 | "border", 179 | "buffered", 180 | "capture", 181 | "challenge", 182 | "charset", 183 | "checked", 184 | "cite", 185 | "class", 186 | "code", 187 | "codebase", 188 | "color", 189 | "cols", 190 | "colspan", 191 | "content", 192 | "contenteditable", 193 | "contextmenu", 194 | "controls", 195 | "coords", 196 | "crossorigin", 197 | "csp", 198 | "data", 199 | "datetime", 200 | "decoding", 201 | "default", 202 | "defer", 203 | "dir", 204 | "dirname", 205 | "disabled", 206 | "download", 207 | "draggable", 208 | "enctype", 209 | "enterkeyhint", 210 | "for", 211 | "form", 212 | "formaction", 213 | "formenctype", 214 | "formmethod", 215 | "formnovalidate", 216 | "formtarget", 217 | "headers", 218 | "height", 219 | "hidden", 220 | "high", 221 | "href", 222 | "hreflang", 223 | "http-equiv", 224 | "icon", 225 | "id", 226 | "importance", 227 | "inputmode", 228 | "integrity", 229 | "intrinsicsize", 230 | "ismap", 231 | "itemprop", 232 | "keytype", 233 | "kind", 234 | "label", 235 | "lang", 236 | "language", 237 | "list", 238 | "loading", 239 | "loop", 240 | "low", 241 | "manifest", 242 | "max", 243 | "maxlength", 244 | "media", 245 | "method", 246 | "min", 247 | "minlength", 248 | "multiple", 249 | "muted", 250 | "name", 251 | "novalidate", 252 | "open", 253 | "optimum", 254 | "pattern", 255 | "ping", 256 | "placeholder", 257 | "poster", 258 | "preload", 259 | "radiogroup", 260 | "readonly", 261 | "referrerpolicy", 262 | "rel", 263 | "required", 264 | "reversed", 265 | "role", 266 | "rows", 267 | "rowspan", 268 | "sandbox", 269 | "scope", 270 | "scoped", 271 | "selected", 272 | "shape", 273 | "size", 274 | "sizes", 275 | "slot", 276 | "span", 277 | "spellcheck", 278 | "src", 279 | "srcdoc", 280 | "srclang", 281 | "srcset", 282 | "start", 283 | "step", 284 | "style", 285 | "summary", 286 | "tabindex", 287 | "target", 288 | "title", 289 | "translate", 290 | "type", 291 | "usemap", 292 | "value", 293 | "width", 294 | "wrap", 295 | ]; 296 | } 297 | } -------------------------------------------------------------------------------- /examples/typed_elements.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | use sledgehammer_bindgen::bindgen; 3 | use wasm_bindgen::prelude::wasm_bindgen; 4 | use web_sys::{console, Node}; 5 | 6 | #[wasm_bindgen(module = "examples/typed_elements.js")] 7 | extern "C" { 8 | #[wasm_bindgen] 9 | pub type TypedElements; 10 | } 11 | 12 | fn main() { 13 | #[bindgen] 14 | mod js { 15 | #[extends(TypedElements)] 16 | struct Channel; 17 | 18 | fn create_element(id: u16, element_id: u8) { 19 | "this.nodes[$id$]=document.createElement(this.els[$element_id$]);" 20 | } 21 | 22 | fn set_attribute(id: u16, attribute_id: u8, val: impl Writable) { 23 | "this.nodes[$id$].setAttribute(this.attrs[$attribute_id$],$val$);" 24 | } 25 | 26 | fn remove_attribute(id: u16, attribute_id: u8) { 27 | "this.nodes[$id$].removeAttribute(this.attrs[$attribute_id$]);" 28 | } 29 | 30 | fn append_child(id: u16, id2: u16) { 31 | "this.nodes[$id$].appendChild(this.nodes[$id2$]);" 32 | } 33 | 34 | fn insert_before(parent: u16, id: u16, id2: u16) { 35 | "this.nodes[$parent$].insertBefore(this.nodes[$id$],this.nodes[$id2$]);" 36 | } 37 | 38 | fn set_text(id: u16, text: impl Writable) { 39 | "this.nodes[$id$].textContent=$text$;" 40 | } 41 | 42 | fn remove(id: u16) { 43 | "this.nodes[$id$].remove();" 44 | } 45 | 46 | fn replace(id: u16, id2: u16) { 47 | "this.nodes[$id$].replaceWith(this.nodes[$id2$]);" 48 | } 49 | 50 | fn clone(id: u16, id2: u16) { 51 | "this.nodes[$id2$]=this.nodes[$id$].cloneNode(true);" 52 | } 53 | 54 | fn first_child(id: u16) { 55 | "this.nodes[id]=this.nodes[id].firstChild;" 56 | } 57 | 58 | fn next_sibling(id: u16) { 59 | "this.nodes[id]=this.nodes[id].nextSibling;" 60 | } 61 | } 62 | 63 | #[wasm_bindgen(inline_js = " 64 | export function get_node(channel, id){ 65 | return channel.nodes[id]; 66 | } 67 | ")] 68 | extern "C" { 69 | fn get_node(node: &JSChannel, id: u16) -> Node; 70 | } 71 | 72 | let mut channel1 = Channel::default(); 73 | let main = 0; 74 | let node1 = 1; 75 | let node2 = 2; 76 | channel1.create_element(node1, Element::div as u8); 77 | channel1.create_element(node2, Element::span as u8); 78 | channel1.append_child(node1, node2); 79 | channel1.set_text(node2, "Hello World!"); 80 | channel1.append_child(main, node1); 81 | channel1.flush(); 82 | 83 | console::log_1(&get_node(channel1.js_channel(), 0).into()); 84 | } 85 | 86 | #[allow(non_camel_case_types)] 87 | #[repr(u8)] 88 | enum Element { 89 | a, 90 | abbr, 91 | acronym, 92 | address, 93 | applet, 94 | area, 95 | article, 96 | aside, 97 | audio, 98 | b, 99 | base, 100 | bdi, 101 | bdo, 102 | bgsound, 103 | big, 104 | blink, 105 | blockquote, 106 | body, 107 | br, 108 | button, 109 | canvas, 110 | caption, 111 | center, 112 | cite, 113 | code, 114 | col, 115 | colgroup, 116 | content, 117 | data, 118 | datalist, 119 | dd, 120 | del, 121 | details, 122 | dfn, 123 | dialog, 124 | dir, 125 | div, 126 | dl, 127 | dt, 128 | em, 129 | embed, 130 | fieldset, 131 | figcaption, 132 | figure, 133 | font, 134 | footer, 135 | form, 136 | frame, 137 | frameset, 138 | h1, 139 | head, 140 | header, 141 | hgroup, 142 | hr, 143 | html, 144 | i, 145 | iframe, 146 | image, 147 | img, 148 | input, 149 | ins, 150 | kbd, 151 | keygen, 152 | label, 153 | legend, 154 | li, 155 | link, 156 | main, 157 | map, 158 | mark, 159 | marquee, 160 | menu, 161 | menuitem, 162 | meta, 163 | meter, 164 | nav, 165 | nobr, 166 | noembed, 167 | noframes, 168 | noscript, 169 | object, 170 | ol, 171 | optgroup, 172 | option, 173 | output, 174 | p, 175 | param, 176 | picture, 177 | plaintext, 178 | portal, 179 | pre, 180 | progress, 181 | q, 182 | rb, 183 | rp, 184 | rt, 185 | rtc, 186 | ruby, 187 | s, 188 | samp, 189 | script, 190 | section, 191 | select, 192 | shadow, 193 | slot, 194 | small, 195 | source, 196 | spacer, 197 | span, 198 | strike, 199 | strong, 200 | style, 201 | sub, 202 | summary, 203 | sup, 204 | table, 205 | tbody, 206 | td, 207 | template, 208 | textarea, 209 | tfoot, 210 | th, 211 | thead, 212 | time, 213 | title, 214 | tr, 215 | track, 216 | tt, 217 | u, 218 | ul, 219 | var, 220 | video, 221 | wbr, 222 | xmp, 223 | } 224 | 225 | #[allow(non_camel_case_types)] 226 | #[repr(u8)] 227 | enum Attribute { 228 | ccept_charset, 229 | accept, 230 | accesskey, 231 | action, 232 | align, 233 | allow, 234 | alt, 235 | aria_atomic, 236 | aria_busy, 237 | aria_controls, 238 | aria_current, 239 | aria_describedby, 240 | aria_description, 241 | aria_details, 242 | aria_disabled, 243 | aria_dropeffect, 244 | aria_errormessage, 245 | aria_flowto, 246 | aria_grabbed, 247 | aria_haspopup, 248 | aria_hidden, 249 | aria_invalid, 250 | aria_keyshortcuts, 251 | aria_label, 252 | aria_labelledby, 253 | aria_live, 254 | aria_owns, 255 | aria_relevant, 256 | aria_roledescription, 257 | r#async, 258 | autocapitalize, 259 | autocomplete, 260 | autofocus, 261 | autoplay, 262 | background, 263 | bgcolor, 264 | border, 265 | buffered, 266 | capture, 267 | challenge, 268 | charset, 269 | checked, 270 | cite, 271 | class, 272 | code, 273 | codebase, 274 | color, 275 | cols, 276 | colspan, 277 | content, 278 | contenteditable, 279 | contextmenu, 280 | controls, 281 | coords, 282 | crossorigin, 283 | csp, 284 | data, 285 | datetime, 286 | decoding, 287 | default, 288 | defer, 289 | dir, 290 | dirname, 291 | disabled, 292 | download, 293 | draggable, 294 | enctype, 295 | enterkeyhint, 296 | r#for, 297 | form, 298 | formaction, 299 | formenctype, 300 | formmethod, 301 | formnovalidate, 302 | formtarget, 303 | headers, 304 | height, 305 | hidden, 306 | high, 307 | href, 308 | hreflang, 309 | http_equiv, 310 | icon, 311 | id, 312 | importance, 313 | inputmode, 314 | integrity, 315 | intrinsicsize, 316 | ismap, 317 | itemprop, 318 | keytype, 319 | kind, 320 | label, 321 | lang, 322 | language, 323 | list, 324 | loading, 325 | r#loop, 326 | low, 327 | manifest, 328 | max, 329 | maxlength, 330 | media, 331 | method, 332 | min, 333 | minlength, 334 | multiple, 335 | muted, 336 | name, 337 | novalidate, 338 | open, 339 | optimum, 340 | pattern, 341 | ping, 342 | placeholder, 343 | poster, 344 | preload, 345 | radiogroup, 346 | readonly, 347 | referrerpolicy, 348 | rel, 349 | required, 350 | reversed, 351 | role, 352 | rows, 353 | rowspan, 354 | sandbox, 355 | scope, 356 | scoped, 357 | selected, 358 | shape, 359 | size, 360 | sizes, 361 | slot, 362 | span, 363 | spellcheck, 364 | src, 365 | srcdoc, 366 | srclang, 367 | srcset, 368 | start, 369 | step, 370 | style, 371 | summary, 372 | tabindex, 373 | target, 374 | title, 375 | translate, 376 | r#type, 377 | usemap, 378 | value, 379 | width, 380 | wrap, 381 | } 382 | -------------------------------------------------------------------------------- /examples/wry.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | use std::{ 3 | cell::Cell, 4 | time::{Duration, Instant}, 5 | }; 6 | 7 | use sledgehammer_bindgen::bindgen; 8 | use wry::http::Response; 9 | 10 | #[cfg(any(target_os = "android", target_os = "windows"))] 11 | const INDEX_PATH: &str = "http://dioxus.index.html"; 12 | 13 | #[cfg(not(any(target_os = "android", target_os = "windows")))] 14 | const INDEX_PATH: &str = "dioxus://index.html"; 15 | 16 | const TYPED_JS: &str = include_str!("./typed_elements.js"); 17 | 18 | #[bindgen] 19 | mod js { 20 | #[extends(TypedElements)] 21 | struct Channel; 22 | 23 | fn create_element(id: u16, element_id: u8) { 24 | "this.nodes[$id$]=document.createElement(this.els[$element_id$]);" 25 | } 26 | 27 | fn set_attribute(id: u16, attribute_id: u8, val: impl Writable) { 28 | "this.nodes[$id$].setAttribute(this.attrs[$attribute_id$],$val$);" 29 | } 30 | 31 | fn remove_attribute(id: u16, attribute_id: u8) { 32 | "this.nodes[$id$].removeAttribute(this.attrs[$attribute_id$]);" 33 | } 34 | 35 | fn append_child(id: u16, id2: u16) { 36 | "this.nodes[$id$].appendChild(this.nodes[$id2$]);" 37 | } 38 | 39 | fn insert_before(parent: u16, id: u16, id2: u16) { 40 | "this.nodes[$parent$].insertBefore(this.nodes[$id$],this.nodes[$id2$]);" 41 | } 42 | 43 | fn set_text(id: u16, text: impl Writable) { 44 | "this.nodes[$id$].textContent=$text$;" 45 | } 46 | 47 | fn remove(id: u16) { 48 | "this.nodes[$id$].remove();" 49 | } 50 | 51 | fn replace(id: u16, id2: u16) { 52 | "this.nodes[$id$].replaceWith(this.nodes[$id2$]);" 53 | } 54 | 55 | fn clone(id: u16, id2: u16) { 56 | "this.nodes[$id2$]=this.nodes[$id$].cloneNode(true);" 57 | } 58 | 59 | fn first_child(id: u16) { 60 | "this.node[id]=this.node[id].firstChild;" 61 | } 62 | 63 | fn next_sibling(id: u16) { 64 | "this.node[id]=this.node[id].nextSibling;" 65 | } 66 | } 67 | 68 | #[allow(non_camel_case_types)] 69 | #[repr(u8)] 70 | enum Element { 71 | a, 72 | abbr, 73 | acronym, 74 | address, 75 | applet, 76 | area, 77 | article, 78 | aside, 79 | audio, 80 | b, 81 | base, 82 | bdi, 83 | bdo, 84 | bgsound, 85 | big, 86 | blink, 87 | blockquote, 88 | body, 89 | br, 90 | button, 91 | canvas, 92 | caption, 93 | center, 94 | cite, 95 | code, 96 | col, 97 | colgroup, 98 | content, 99 | data, 100 | datalist, 101 | dd, 102 | del, 103 | details, 104 | dfn, 105 | dialog, 106 | dir, 107 | div, 108 | dl, 109 | dt, 110 | em, 111 | embed, 112 | fieldset, 113 | figcaption, 114 | figure, 115 | font, 116 | footer, 117 | form, 118 | frame, 119 | frameset, 120 | h1, 121 | head, 122 | header, 123 | hgroup, 124 | hr, 125 | html, 126 | i, 127 | iframe, 128 | image, 129 | img, 130 | input, 131 | ins, 132 | kbd, 133 | keygen, 134 | label, 135 | legend, 136 | li, 137 | link, 138 | main, 139 | map, 140 | mark, 141 | marquee, 142 | menu, 143 | menuitem, 144 | meta, 145 | meter, 146 | nav, 147 | nobr, 148 | noembed, 149 | noframes, 150 | noscript, 151 | object, 152 | ol, 153 | optgroup, 154 | option, 155 | output, 156 | p, 157 | param, 158 | picture, 159 | plaintext, 160 | portal, 161 | pre, 162 | progress, 163 | q, 164 | rb, 165 | rp, 166 | rt, 167 | rtc, 168 | ruby, 169 | s, 170 | samp, 171 | script, 172 | section, 173 | select, 174 | shadow, 175 | slot, 176 | small, 177 | source, 178 | spacer, 179 | span, 180 | strike, 181 | strong, 182 | style, 183 | sub, 184 | summary, 185 | sup, 186 | table, 187 | tbody, 188 | td, 189 | template, 190 | textarea, 191 | tfoot, 192 | th, 193 | thead, 194 | time, 195 | title, 196 | tr, 197 | track, 198 | tt, 199 | u, 200 | ul, 201 | var, 202 | video, 203 | wbr, 204 | xmp, 205 | } 206 | 207 | #[allow(non_camel_case_types)] 208 | #[repr(u8)] 209 | enum Attribute { 210 | ccept_charset, 211 | accept, 212 | accesskey, 213 | action, 214 | align, 215 | allow, 216 | alt, 217 | aria_atomic, 218 | aria_busy, 219 | aria_controls, 220 | aria_current, 221 | aria_describedby, 222 | aria_description, 223 | aria_details, 224 | aria_disabled, 225 | aria_dropeffect, 226 | aria_errormessage, 227 | aria_flowto, 228 | aria_grabbed, 229 | aria_haspopup, 230 | aria_hidden, 231 | aria_invalid, 232 | aria_keyshortcuts, 233 | aria_label, 234 | aria_labelledby, 235 | aria_live, 236 | aria_owns, 237 | aria_relevant, 238 | aria_roledescription, 239 | r#async, 240 | autocapitalize, 241 | autocomplete, 242 | autofocus, 243 | autoplay, 244 | background, 245 | bgcolor, 246 | border, 247 | buffered, 248 | capture, 249 | challenge, 250 | charset, 251 | checked, 252 | cite, 253 | class, 254 | code, 255 | codebase, 256 | color, 257 | cols, 258 | colspan, 259 | content, 260 | contenteditable, 261 | contextmenu, 262 | controls, 263 | coords, 264 | crossorigin, 265 | csp, 266 | data, 267 | datetime, 268 | decoding, 269 | default, 270 | defer, 271 | dir, 272 | dirname, 273 | disabled, 274 | download, 275 | draggable, 276 | enctype, 277 | enterkeyhint, 278 | r#for, 279 | form, 280 | formaction, 281 | formenctype, 282 | formmethod, 283 | formnovalidate, 284 | formtarget, 285 | headers, 286 | height, 287 | hidden, 288 | high, 289 | href, 290 | hreflang, 291 | http_equiv, 292 | icon, 293 | id, 294 | importance, 295 | inputmode, 296 | integrity, 297 | intrinsicsize, 298 | ismap, 299 | itemprop, 300 | keytype, 301 | kind, 302 | label, 303 | lang, 304 | language, 305 | list, 306 | loading, 307 | r#loop, 308 | low, 309 | manifest, 310 | max, 311 | maxlength, 312 | media, 313 | method, 314 | min, 315 | minlength, 316 | multiple, 317 | muted, 318 | name, 319 | novalidate, 320 | open, 321 | optimum, 322 | pattern, 323 | ping, 324 | placeholder, 325 | poster, 326 | preload, 327 | radiogroup, 328 | readonly, 329 | referrerpolicy, 330 | rel, 331 | required, 332 | reversed, 333 | role, 334 | rows, 335 | rowspan, 336 | sandbox, 337 | scope, 338 | scoped, 339 | selected, 340 | shape, 341 | size, 342 | sizes, 343 | slot, 344 | span, 345 | spellcheck, 346 | src, 347 | srcdoc, 348 | srclang, 349 | srcset, 350 | start, 351 | step, 352 | style, 353 | summary, 354 | tabindex, 355 | target, 356 | title, 357 | translate, 358 | r#type, 359 | usemap, 360 | value, 361 | width, 362 | wrap, 363 | } 364 | 365 | fn main() -> wry::Result<()> { 366 | use wry::{ 367 | application::{ 368 | event::{Event, StartCause, WindowEvent}, 369 | event_loop::{ControlFlow, EventLoop}, 370 | window::WindowBuilder, 371 | }, 372 | webview::WebViewBuilder, 373 | }; 374 | 375 | let event_loop = EventLoop::new(); 376 | let window = WindowBuilder::new() 377 | .with_title("Hello World") 378 | .build(&event_loop)?; 379 | let first_request = Cell::new(true); 380 | let _webview = WebViewBuilder::new(window)? 381 | .with_url(INDEX_PATH) 382 | .unwrap() 383 | .with_asynchronous_custom_protocol("dioxus".into(), move |_, responder| { 384 | if first_request.get() { 385 | first_request.set(false); 386 | let html = format!( 387 | r#" 388 | 389 | 390 | 391 |
392 | 412 | 413 | "#, 414 | TYPED_JS, 415 | GENERATED_JS.replace("export", "") 416 | ); 417 | responder.respond( 418 | Response::builder() 419 | .header("Access-Control-Allow-Origin", "*") 420 | .header("Content-Type", "text/html") 421 | .body(html.as_bytes().to_vec()) 422 | .unwrap(), 423 | ); 424 | return; 425 | } 426 | 427 | let mut channel1 = Channel::default(); 428 | let main = 0; 429 | let node1 = 1; 430 | let node2 = 2; 431 | for _ in 0..rand::random::() { 432 | channel1.create_element(node1, Element::div as u8); 433 | channel1.create_element(node2, Element::span as u8); 434 | channel1.append_child(node1, node2); 435 | let rand1 = rand::random::(); 436 | let rand2 = rand::random::(); 437 | channel1.set_text( 438 | node2, 439 | format_args!("{}+{}={}", rand1, rand2, rand1 as usize + rand2 as usize), 440 | ); 441 | channel1.append_child(main, node1); 442 | } 443 | 444 | let data = channel1.export_memory(); 445 | let data: Vec<_> = data.collect(); 446 | println!("{:?}", data); 447 | 448 | channel1.reset(); 449 | 450 | std::thread::spawn(move || { 451 | std::thread::sleep(Duration::from_millis(100)); 452 | responder.respond(Response::new(data)); 453 | }); 454 | }) 455 | .build()?; 456 | 457 | event_loop.run(move |event, _, control_flow| { 458 | *control_flow = ControlFlow::Wait; 459 | 460 | match event { 461 | Event::NewEvents(StartCause::Init) => {} 462 | Event::WindowEvent { 463 | event: WindowEvent::CloseRequested, 464 | .. 465 | } => *control_flow = ControlFlow::Exit, 466 | _ => (), 467 | } 468 | }); 469 | } 470 | -------------------------------------------------------------------------------- /sledgehammer_bindgen_macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sledgehammer_bindgen_macro" 3 | version = "0.6.1" 4 | authors = ["Evan Almloff "] 5 | edition = "2021" 6 | description = "Fast batched js bindings" 7 | documentation = "https://docs.rs/sledgehammer_bindgen" 8 | readme = "../README.md" 9 | repository = "https://github.com/demonthos/sledgehammer_bindgen/" 10 | license = "MIT" 11 | keywords = ["web", "wasm", "dom"] 12 | categories = ["web-programming", "wasm", "api-bindings"] 13 | 14 | [dependencies] 15 | syn = { version = "2.0", features = ["full", "extra-traits"] } 16 | quote = "1.0" 17 | 18 | [lib] 19 | proc-macro = true 20 | 21 | [dev-dependencies.web-sys] 22 | version = "0.3" 23 | features = ["Node", "console"] 24 | 25 | [features] 26 | default = [] 27 | web = [] 28 | -------------------------------------------------------------------------------- /sledgehammer_bindgen_macro/src/builder.rs: -------------------------------------------------------------------------------- 1 | use quote::{__private::TokenStream as TokenStream2, quote}; 2 | use syn::{parse_quote, Expr, Ident}; 3 | 4 | use crate::select_bits_js_inner; 5 | 6 | #[derive(Default)] 7 | pub struct BindingBuilder { 8 | js_u32_count: usize, 9 | js_flag_count: usize, 10 | } 11 | 12 | impl BindingBuilder { 13 | pub fn u32(&mut self) -> RustJSU32 { 14 | let id = self.js_u32_count; 15 | self.js_u32_count += 1; 16 | RustJSU32 { id } 17 | } 18 | 19 | pub fn flag(&mut self) -> RustJSFlag { 20 | let id = self.js_flag_count; 21 | self.js_flag_count += 1; 22 | RustJSFlag { id } 23 | } 24 | 25 | pub fn rust_ident(&self) -> Ident { 26 | parse_quote!(metadata) 27 | } 28 | 29 | pub fn rust_type(&self) -> TokenStream2 { 30 | let len = self.js_u32_count + 1; 31 | quote! { 32 | std::pin::Pin; #len]>> 33 | } 34 | } 35 | 36 | pub fn rust_init(&self) -> TokenStream2 { 37 | quote! { 38 | std::boxed::Box::pin(Default::default()) 39 | } 40 | } 41 | 42 | pub fn pre_run_js(&self) -> String { 43 | "this.metaflags=this.m.getUint32(this.d,true);".to_string() 44 | } 45 | } 46 | 47 | pub struct RustJSU32 { 48 | id: usize, 49 | } 50 | 51 | impl RustJSU32 { 52 | pub fn read_js(&self) -> String { 53 | format!("this.m.getUint32(this.d+{}*4,true)", self.id + 1) 54 | } 55 | 56 | pub fn write_rust(&self, value: Expr) -> TokenStream2 { 57 | let id = self.id; 58 | 59 | quote! { 60 | unsafe {self.metadata.get_unchecked(#id + 1)}.set(#value); 61 | } 62 | } 63 | 64 | pub fn get_rust(&self) -> TokenStream2 { 65 | let id = self.id; 66 | 67 | quote! { 68 | unsafe {self.metadata.get_unchecked(#id + 1)}.get() 69 | } 70 | } 71 | } 72 | 73 | pub struct RustJSFlag { 74 | id: usize, 75 | } 76 | 77 | impl RustJSFlag { 78 | pub fn read_js(&self) -> String { 79 | select_bits_js_inner("this.metaflags", 32, self.id, 1) 80 | } 81 | 82 | pub fn write_rust(&self, value: Expr) -> TokenStream2 { 83 | let id = self.id; 84 | 85 | quote! { 86 | unsafe {self.metadata.get_unchecked(0).set(if #value { self.metadata.get_unchecked(0).get() | (1 << #id) } else { self.metadata.get_unchecked(0).get() & !(1 << #id) })}; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /sledgehammer_bindgen_macro/src/encoder.rs: -------------------------------------------------------------------------------- 1 | use std::{any::Any, collections::BTreeMap, ops::Deref, rc::Rc}; 2 | 3 | use quote::{__private::TokenStream as TokenStream2, quote}; 4 | use syn::{Ident, Type}; 5 | 6 | use crate::builder::BindingBuilder; 7 | 8 | pub trait CreateEncoder { 9 | type Output; 10 | 11 | fn create(&self, encoder: &mut Encoders) -> Self::Output; 12 | 13 | fn rust_ident(&self) -> Ident; 14 | } 15 | 16 | pub trait Encoder { 17 | fn initializer(&self) -> String { 18 | String::new() 19 | } 20 | 21 | fn pre_run_js(&self) -> String { 22 | String::new() 23 | } 24 | 25 | fn rust_type(&self) -> Type; 26 | 27 | fn rust_ident(&self) -> Ident; 28 | 29 | fn global_rust(&self) -> TokenStream2 { 30 | quote!() 31 | } 32 | 33 | fn init_rust(&self) -> TokenStream2 { 34 | quote!() 35 | } 36 | 37 | fn memory_moved_rust(&self) -> TokenStream2 { 38 | quote!() 39 | } 40 | 41 | fn pre_run_rust(&self) -> TokenStream2 { 42 | quote!() 43 | } 44 | 45 | fn post_run_rust(&self) -> TokenStream2 { 46 | quote!() 47 | } 48 | 49 | fn merge_memory_rust(&self) -> TokenStream2 { 50 | quote! { 51 | std::iter::empty() 52 | } 53 | } 54 | } 55 | 56 | pub trait Encode { 57 | fn encode_js(&self) -> String; 58 | 59 | fn encode_rust(&self, ident: &Ident) -> TokenStream2; 60 | } 61 | 62 | pub trait DynEncode: Any + Encode + Encoder {} 63 | 64 | impl DynEncode for T {} 65 | 66 | pub trait AnyDynEncode: DynEncode { 67 | fn as_any(&self) -> &dyn Any; 68 | } 69 | 70 | impl AnyDynEncode for T { 71 | fn as_any(&self) -> &dyn Any { 72 | self 73 | } 74 | } 75 | 76 | #[derive(Clone)] 77 | pub struct EncodeTraitObject(Rc); 78 | 79 | impl EncodeTraitObject { 80 | pub fn downcast(&self) -> &T { 81 | let rc_any = &*self.0; 82 | rc_any.as_any().downcast_ref::().unwrap() 83 | } 84 | } 85 | 86 | impl Encoder for EncodeTraitObject { 87 | fn initializer(&self) -> String { 88 | self.0.initializer() 89 | } 90 | 91 | fn pre_run_js(&self) -> String { 92 | self.0.pre_run_js() 93 | } 94 | 95 | fn rust_type(&self) -> Type { 96 | self.0.rust_type() 97 | } 98 | 99 | fn rust_ident(&self) -> Ident { 100 | self.0.rust_ident() 101 | } 102 | 103 | fn global_rust(&self) -> TokenStream2 { 104 | self.0.global_rust() 105 | } 106 | 107 | fn memory_moved_rust(&self) -> TokenStream2 { 108 | self.0.memory_moved_rust() 109 | } 110 | 111 | fn init_rust(&self) -> TokenStream2 { 112 | self.0.init_rust() 113 | } 114 | 115 | fn pre_run_rust(&self) -> TokenStream2 { 116 | self.0.pre_run_rust() 117 | } 118 | 119 | fn post_run_rust(&self) -> TokenStream2 { 120 | self.0.post_run_rust() 121 | } 122 | 123 | fn merge_memory_rust(&self) -> TokenStream2 { 124 | self.0.merge_memory_rust() 125 | } 126 | } 127 | 128 | impl Encode for EncodeTraitObject { 129 | fn encode_js(&self) -> String { 130 | self.0.encode_js() 131 | } 132 | 133 | fn encode_rust(&self, ident: &Ident) -> TokenStream2 { 134 | self.0.encode_rust(ident) 135 | } 136 | } 137 | 138 | #[derive(Default)] 139 | pub struct Encoders { 140 | encoders: BTreeMap, 141 | pub(crate) builder: BindingBuilder, 142 | } 143 | 144 | impl Deref for Encoders { 145 | type Target = BTreeMap; 146 | 147 | fn deref(&self) -> &Self::Target { 148 | &self.encoders 149 | } 150 | } 151 | 152 | impl Encoders { 153 | pub fn insert, O: AnyDynEncode>(&mut self, factory: T) { 154 | self.get_or_insert_with(factory); 155 | } 156 | 157 | pub fn get_or_insert_with, O: AnyDynEncode>( 158 | &mut self, 159 | factory: T, 160 | ) -> EncodeTraitObject { 161 | let id = factory.rust_ident(); 162 | let value = self.encoders.get(&id); 163 | match value { 164 | Some(value) => value.clone(), 165 | None => { 166 | let value = EncodeTraitObject(Rc::new(factory.create(self))); 167 | self.encoders.insert(id.clone(), value.clone()); 168 | value 169 | } 170 | } 171 | } 172 | 173 | pub fn builder(&mut self) -> &mut BindingBuilder { 174 | &mut self.builder 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /sledgehammer_bindgen_macro/src/function.rs: -------------------------------------------------------------------------------- 1 | use crate::types::{ 2 | numbers::NumberEncoderFactory, 3 | slice::SliceFactory, 4 | string::{GeneralStringFactory, StrEncoderFactory}, 5 | writable::WritableEncoderFactory, 6 | }; 7 | use std::fmt::Write; 8 | use std::ops::Deref; 9 | 10 | use quote::{__private::TokenStream as TokenStream2, quote}; 11 | use syn::{ 12 | parse_quote, Expr, GenericArgument, Ident, ItemFn, Lit, Pat, PathArguments, PathSegment, Type, 13 | TypeParamBound, 14 | }; 15 | 16 | use crate::{ 17 | encoder::{Encode, EncodeTraitObject, Encoders}, 18 | types::numbers, 19 | }; 20 | 21 | pub struct FunctionBinding { 22 | attributes: Vec, 23 | name: Ident, 24 | type_encodings: Vec, 25 | encoding_order: Vec, 26 | js_output: String, 27 | pub(crate) variables: Vec, 28 | } 29 | 30 | struct TypeEncoding { 31 | ty: Type, 32 | ident: Ident, 33 | decode_js: String, 34 | encode_rust: TokenStream2, 35 | } 36 | 37 | impl TypeEncoding { 38 | fn new(ident: &Ident, ty: Type, encoder: &EncodeTraitObject) -> Self { 39 | Self { 40 | ty, 41 | ident: ident.clone(), 42 | decode_js: encoder.encode_js(), 43 | encode_rust: encoder.encode_rust(ident), 44 | } 45 | } 46 | } 47 | 48 | impl FunctionBinding { 49 | pub fn new(encoders: &mut Encoders, function: ItemFn) -> syn::Result { 50 | let name = function.sig.ident; 51 | let mut myself = Self { 52 | name, 53 | attributes: function.attrs, 54 | type_encodings: Vec::new(), 55 | encoding_order: Vec::new(), 56 | variables: Vec::new(), 57 | js_output: String::new(), 58 | }; 59 | 60 | for arg in function.sig.inputs { 61 | match arg { 62 | syn::FnArg::Receiver(_) => { 63 | return Err(syn::Error::new_spanned( 64 | arg, 65 | "self is not supported in bindgen functions", 66 | )) 67 | } 68 | syn::FnArg::Typed(ty) => { 69 | let ident = if let Pat::Ident(i) = &*ty.pat { 70 | &i.ident 71 | } else { 72 | return Err(syn::Error::new_spanned( 73 | ty.pat, 74 | "only simple identifiers are supported", 75 | )); 76 | }; 77 | myself.add(encoders, ident, ty.ty.deref().clone()) 78 | } 79 | } 80 | } 81 | 82 | let body = if let &[syn::Stmt::Expr(Expr::Lit(lit), _)] = &function.block.stmts.as_slice() { 83 | if let Lit::Str(s) = &lit.lit { 84 | s.value() 85 | } else { 86 | return Err(syn::Error::new_spanned( 87 | lit, 88 | "only string literals are supported as function bodies", 89 | )); 90 | } 91 | } else { 92 | return Err(syn::Error::new_spanned( 93 | &function.block.stmts[0], 94 | "only a single string literal is supported as function bodies", 95 | )); 96 | }; 97 | 98 | // Inline every parameter we can 99 | let body = parse_js_body(&body, |parameter| { 100 | // We need to reorder the javascript_decodings to match the order of the parameters 101 | let idx = myself 102 | .type_encodings 103 | .iter() 104 | .position(|e| e.ident == parameter) 105 | .unwrap_or_else(|| { 106 | panic!("attempted to inline an unknown parameter: {}", parameter) 107 | }); 108 | let encoding = &myself.type_encodings[idx]; 109 | parameter.clone_from(&encoding.decode_js); 110 | myself.encoding_order.push(idx); 111 | }); 112 | 113 | // Any remaining parameters are not inlined and must be decoded first 114 | let non_inlined_parameter_indices: Vec<_> = (0..myself.type_encodings.len()) 115 | .filter(|idx| !myself.encoding_order.contains(idx)) 116 | .collect(); 117 | myself.encoding_order = non_inlined_parameter_indices 118 | .iter() 119 | .copied() 120 | .chain(myself.encoding_order.iter().copied()) 121 | .collect(); 122 | 123 | let javascript_decodings: Vec<_> = myself 124 | .type_encodings 125 | .iter() 126 | .enumerate() 127 | .filter(|(idx, _)| non_inlined_parameter_indices.contains(idx)) 128 | .map(|(_, encoding)| (encoding.ident.to_string(), encoding.decode_js.clone())) 129 | .collect(); 130 | 131 | myself.variables = javascript_decodings 132 | .iter() 133 | .map(|(k, _)| k.to_string()) 134 | .collect(); 135 | 136 | let unmatched_decodings: String = 137 | javascript_decodings 138 | .into_iter() 139 | .fold(String::new(), |mut state, (k, v)| { 140 | let _ = write!(state, "{}={};", k, v); 141 | state 142 | }); 143 | 144 | myself.js_output = unmatched_decodings; 145 | 146 | myself.js_output += &body; 147 | 148 | Ok(myself) 149 | } 150 | 151 | pub fn js(&self) -> &str { 152 | &self.js_output 153 | } 154 | 155 | pub fn to_tokens(&self, index: u8) -> TokenStream2 { 156 | let name = &self.name; 157 | let args = self.type_encodings.iter().map(|encoding| &encoding.ident); 158 | let types = self.type_encodings.iter().map(|encoding| &encoding.ty); 159 | let encode = self.encoding_order.iter().map(|idx| { 160 | let encoding = &self.type_encodings[*idx]; 161 | &encoding.encode_rust 162 | }); 163 | let attrs = &self.attributes; 164 | 165 | quote! { 166 | #(#attrs)* 167 | #[allow(clippy::uninit_vec)] 168 | pub fn #name(&mut self, #(#args: #types),*) { 169 | self.encode_op(#index); 170 | #(#encode)* 171 | } 172 | } 173 | } 174 | 175 | fn add(&mut self, encoders: &mut Encoders, ident: &Ident, ty: Type) { 176 | if let Type::Path(segments) = &ty { 177 | let segments: Vec<_> = segments.path.segments.iter().collect(); 178 | if let &[simple] = segments.as_slice() { 179 | let as_str = simple.ident.to_string(); 180 | let encoder = match as_str.as_str() { 181 | "u8" => encoders.get_or_insert_with(numbers::NumberEncoderFactory::<1>), 182 | "u16" => encoders.get_or_insert_with(numbers::NumberEncoderFactory::<2>), 183 | "u32" => encoders.get_or_insert_with(numbers::NumberEncoderFactory::<4>), 184 | _ => panic!("unsupported type"), 185 | }; 186 | 187 | let type_encoding = TypeEncoding::new(ident, ty, &encoder); 188 | self.type_encodings.push(type_encoding); 189 | return; 190 | } 191 | } else if let Type::Reference(ty_ref) = &ty { 192 | let static_lifetime = ty_ref 193 | .lifetime 194 | .as_ref() 195 | .filter(|l| l.ident == "static") 196 | .is_some(); 197 | if let Type::Path(segments) = &*ty_ref.elem { 198 | let ty = if static_lifetime { 199 | parse_quote!(&'static str) 200 | } else { 201 | parse_quote!(&str) 202 | }; 203 | let segments: Vec<_> = segments.path.segments.iter().collect(); 204 | if let &[simple] = segments.as_slice() { 205 | let as_str = simple.ident.to_string().to_lowercase(); 206 | if as_str == "str" { 207 | let mut cache = None; 208 | if let PathArguments::AngleBracketed(gen) = &simple.arguments { 209 | let generics: Vec<_> = gen.args.iter().collect(); 210 | if let GenericArgument::Type(Type::Path(t)) = &generics[0] { 211 | let segments: Vec<_> = t.path.segments.iter().collect(); 212 | if let &[simple] = segments.as_slice() { 213 | if let Some(GenericArgument::Type(Type::Path(t))) = 214 | &generics.get(1) 215 | { 216 | let segments: Vec<_> = t.path.segments.iter().collect(); 217 | if let &[simple] = segments.as_slice() { 218 | cache = Some(simple.ident.clone()); 219 | } 220 | } 221 | let encoder = match simple.ident.to_string().as_str() { 222 | "u8" => { 223 | encoders.insert(NumberEncoderFactory::<1>); 224 | encoders.get_or_insert_with(StrEncoderFactory::<1> { 225 | cache_name: cache, 226 | static_str: static_lifetime, 227 | }) 228 | } 229 | "u16" => { 230 | encoders.insert(NumberEncoderFactory::<2>); 231 | encoders.get_or_insert_with(StrEncoderFactory::<2> { 232 | cache_name: cache, 233 | static_str: static_lifetime, 234 | }) 235 | } 236 | "u32" => { 237 | encoders.insert(NumberEncoderFactory::<4>); 238 | encoders.get_or_insert_with(StrEncoderFactory::<4> { 239 | cache_name: cache, 240 | static_str: static_lifetime, 241 | }) 242 | } 243 | _ => panic!("unsupported type"), 244 | }; 245 | 246 | let type_encoding = TypeEncoding::new(ident, ty, &encoder); 247 | self.type_encodings.push(type_encoding); 248 | return; 249 | } 250 | } 251 | } 252 | encoders.insert(NumberEncoderFactory::<4>); 253 | let encoder = encoders.get_or_insert_with(StrEncoderFactory::<4> { 254 | cache_name: cache, 255 | static_str: static_lifetime, 256 | }); 257 | 258 | let type_encoding = TypeEncoding::new(ident, ty, &encoder); 259 | self.type_encodings.push(type_encoding); 260 | return; 261 | } 262 | } 263 | } 264 | if let Type::Slice(slice) = &*ty_ref.elem { 265 | if let Type::Path(segments) = &*slice.elem { 266 | let segments: Vec<_> = segments.path.segments.iter().collect(); 267 | if let &[simple] = segments.as_slice() { 268 | let as_str = simple.ident.to_string(); 269 | if let PathArguments::AngleBracketed(gen) = &simple.arguments { 270 | let generics: Vec<_> = gen.args.iter().collect(); 271 | if let &[GenericArgument::Type(Type::Path(t))] = generics.as_slice() { 272 | let segments: Vec<_> = t.path.segments.iter().collect(); 273 | if let &[simple] = segments.as_slice() { 274 | let encoder = match simple.ident.to_string().as_str() { 275 | "u8" => match as_str.as_str() { 276 | "u8" => { 277 | encoders.insert(NumberEncoderFactory::<1>); 278 | if static_lifetime { 279 | encoders.get_or_insert_with( 280 | SliceFactory::<1, 1, true>, 281 | ) 282 | } else { 283 | encoders.get_or_insert_with( 284 | SliceFactory::<1, 1, false>, 285 | ) 286 | } 287 | } 288 | "u16" => { 289 | encoders.insert(NumberEncoderFactory::<2>); 290 | if static_lifetime { 291 | encoders.get_or_insert_with( 292 | SliceFactory::<2, 1, true>, 293 | ) 294 | } else { 295 | encoders.get_or_insert_with( 296 | SliceFactory::<2, 1, false>, 297 | ) 298 | } 299 | } 300 | "u32" => { 301 | encoders.insert(NumberEncoderFactory::<4>); 302 | if static_lifetime { 303 | encoders.get_or_insert_with( 304 | SliceFactory::<4, 1, true>, 305 | ) 306 | } else { 307 | encoders.get_or_insert_with( 308 | SliceFactory::<4, 1, false>, 309 | ) 310 | } 311 | } 312 | _ => panic!("unsupported type"), 313 | }, 314 | "u16" => match as_str.as_str() { 315 | "u8" => { 316 | encoders.insert(NumberEncoderFactory::<1>); 317 | if static_lifetime { 318 | encoders.get_or_insert_with( 319 | SliceFactory::<1, 2, true>, 320 | ) 321 | } else { 322 | encoders.get_or_insert_with( 323 | SliceFactory::<1, 2, false>, 324 | ) 325 | } 326 | } 327 | "u16" => { 328 | encoders.insert(NumberEncoderFactory::<2>); 329 | if static_lifetime { 330 | encoders.get_or_insert_with( 331 | SliceFactory::<2, 2, true>, 332 | ) 333 | } else { 334 | encoders.get_or_insert_with( 335 | SliceFactory::<2, 2, false>, 336 | ) 337 | } 338 | } 339 | "u32" => { 340 | encoders.insert(NumberEncoderFactory::<4>); 341 | if static_lifetime { 342 | encoders.get_or_insert_with( 343 | SliceFactory::<4, 2, true>, 344 | ) 345 | } else { 346 | encoders.get_or_insert_with( 347 | SliceFactory::<4, 2, false>, 348 | ) 349 | } 350 | } 351 | _ => panic!("unsupported type"), 352 | }, 353 | "u32" => match as_str.as_str() { 354 | "u8" => { 355 | encoders.insert(NumberEncoderFactory::<1>); 356 | if static_lifetime { 357 | encoders.get_or_insert_with( 358 | SliceFactory::<1, 4, true>, 359 | ) 360 | } else { 361 | encoders.get_or_insert_with( 362 | SliceFactory::<1, 4, false>, 363 | ) 364 | } 365 | } 366 | "u16" => { 367 | encoders.insert(NumberEncoderFactory::<2>); 368 | if static_lifetime { 369 | encoders.get_or_insert_with( 370 | SliceFactory::<2, 4, true>, 371 | ) 372 | } else { 373 | encoders.get_or_insert_with( 374 | SliceFactory::<2, 4, false>, 375 | ) 376 | } 377 | } 378 | "u32" => { 379 | encoders.insert(NumberEncoderFactory::<4>); 380 | if static_lifetime { 381 | encoders.get_or_insert_with( 382 | SliceFactory::<4, 4, true>, 383 | ) 384 | } else { 385 | encoders.get_or_insert_with( 386 | SliceFactory::<4, 4, false>, 387 | ) 388 | } 389 | } 390 | _ => panic!("unsupported type"), 391 | }, 392 | _ => panic!("unsupported type"), 393 | }; 394 | 395 | let type_encoding = TypeEncoding::new(ident, ty, &encoder); 396 | self.type_encodings.push(type_encoding); 397 | return; 398 | } 399 | } 400 | } else { 401 | let encoder = match as_str.as_str() { 402 | "u8" => { 403 | encoders.insert(NumberEncoderFactory::<1>); 404 | if static_lifetime { 405 | encoders.get_or_insert_with(SliceFactory::<1, 4, true>) 406 | } else { 407 | encoders.get_or_insert_with(SliceFactory::<1, 4, false>) 408 | } 409 | } 410 | "u16" => { 411 | encoders.insert(NumberEncoderFactory::<2>); 412 | if static_lifetime { 413 | encoders.get_or_insert_with(SliceFactory::<2, 4, true>) 414 | } else { 415 | encoders.get_or_insert_with(SliceFactory::<2, 4, false>) 416 | } 417 | } 418 | "u32" => { 419 | encoders.insert(NumberEncoderFactory::<4>); 420 | if static_lifetime { 421 | encoders.get_or_insert_with(SliceFactory::<4, 4, true>) 422 | } else { 423 | encoders.get_or_insert_with(SliceFactory::<4, 4, false>) 424 | } 425 | } 426 | _ => panic!("unsupported type"), 427 | }; 428 | let type_encoding = TypeEncoding::new(ident, ty, &encoder); 429 | self.type_encodings.push(type_encoding); 430 | return; 431 | } 432 | } 433 | } 434 | } 435 | } else if let Type::ImplTrait(tr) = &ty { 436 | let traits: Vec<_> = tr.bounds.iter().collect(); 437 | if let &[TypeParamBound::Trait(tr)] = traits.as_slice() { 438 | let segments: Vec<_> = tr.path.segments.iter().collect(); 439 | if let &[simple] = segments.as_slice() { 440 | if simple.ident == "Writable" { 441 | if let PathArguments::AngleBracketed(gen) = &simple.arguments { 442 | let generics: Vec<_> = gen.args.iter().collect(); 443 | if let &[GenericArgument::Type(Type::Path(t))] = generics.as_slice() { 444 | let segments: Vec<_> = t.path.segments.iter().collect(); 445 | if let &[simple] = segments.as_slice() { 446 | encoders.insert(GeneralStringFactory); 447 | match simple { 448 | PathSegment { 449 | ident: as_string, 450 | arguments: PathArguments::None, 451 | } => { 452 | let encoder = match as_string.to_string().as_str() { 453 | "u8" => { 454 | encoders.insert(NumberEncoderFactory::<1>); 455 | encoders.get_or_insert_with( 456 | WritableEncoderFactory::<1>, 457 | ) 458 | } 459 | "u16" => { 460 | encoders.insert(NumberEncoderFactory::<2>); 461 | encoders.get_or_insert_with( 462 | WritableEncoderFactory::<2>, 463 | ) 464 | } 465 | "u32" => { 466 | encoders.insert(NumberEncoderFactory::<4>); 467 | encoders.get_or_insert_with( 468 | WritableEncoderFactory::<4>, 469 | ) 470 | } 471 | _ => panic!("unsupported type"), 472 | }; 473 | let type_encoding = TypeEncoding::new( 474 | ident, 475 | parse_quote!(impl sledgehammer_utils::Writable), 476 | &encoder, 477 | ); 478 | self.type_encodings.push(type_encoding); 479 | return; 480 | } 481 | _ => panic!("unsupported type"), 482 | } 483 | } 484 | } 485 | } else { 486 | encoders.insert(GeneralStringFactory); 487 | encoders.insert(NumberEncoderFactory::<4>); 488 | let encoder = encoders.get_or_insert_with(WritableEncoderFactory::<4>); 489 | let type_encoding = TypeEncoding::new( 490 | ident, 491 | parse_quote!(impl sledgehammer_utils::Writable), 492 | &encoder, 493 | ); 494 | self.type_encodings.push(type_encoding); 495 | return; 496 | } 497 | } 498 | } 499 | } 500 | } 501 | panic!("unsupported type") 502 | } 503 | } 504 | 505 | fn parse_js_body(s: &str, mut f: impl FnMut(&mut String)) -> String { 506 | let mut inside_param = false; 507 | let mut last_was_escape = false; 508 | let mut current_param = String::new(); 509 | let mut current_text = String::new(); 510 | for c in s.chars() { 511 | match c { 512 | '\\' => last_was_escape = true, 513 | '$' => { 514 | if last_was_escape { 515 | if inside_param { 516 | current_param.push(c); 517 | } else { 518 | current_text.push(c); 519 | } 520 | last_was_escape = false; 521 | } else { 522 | if inside_param { 523 | // segments.push((current_segment, current_param)); 524 | f(&mut current_param); 525 | current_text += ¤t_param; 526 | current_param = String::new(); 527 | } 528 | inside_param = !inside_param; 529 | } 530 | } 531 | _ => { 532 | last_was_escape = false; 533 | if inside_param { 534 | current_param.push(c); 535 | } else { 536 | current_text.push(c); 537 | } 538 | } 539 | } 540 | } 541 | current_text 542 | } 543 | 544 | #[test] 545 | fn replace_vars() { 546 | let output = parse_js_body("hello $world$ this is $a$ test", |s| { 547 | *s = s.to_uppercase(); 548 | }); 549 | 550 | assert_eq!(output, "hello WORLD this is A test"); 551 | } 552 | -------------------------------------------------------------------------------- /sledgehammer_bindgen_macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | //!
2 | //!

sledgehammer bindgen

3 | //!
4 | //!
5 | //! 6 | //! 7 | //! Crates.io version 9 | //! 10 | //! 11 | //! 12 | //! Download 14 | //! 15 | //! 16 | //! 17 | //! docs.rs docs 19 | //! 20 | //!
21 | //! 22 | //! # What is Sledgehammer Bindgen? 23 | //! Sledgehammer bindgen provides faster rust batched bindings into javascript code. 24 | //! 25 | //! # How does this compare to wasm-bindgen: 26 | //! - wasm-bindgen is a lot more general it allows returning values and passing around a lot more different types of values. For most users wasm-bindgen is a beter choice. Sledgehammer bindgen is specifically that want low-level, fast access to javascript. 27 | //! 28 | //! - You can use sledgehammer bindgen with wasm-bindgen. See the docs and examples for more information. 29 | //! 30 | //! # Why is it fast? 31 | //! 32 | //! ## String decoding 33 | //! 34 | //! - Decoding strings are expensive to decode, but the cost doesn't change much with the size of the string. Wasm-bindgen calls TextDecoder.decode for every string. Sledgehammer only calls TextEncoder.decode once per batch. 35 | //! 36 | //! - If the string is small, it is faster to decode the string in javascript to avoid the constant overhead of TextDecoder.decode 37 | //! 38 | //! - See this benchmark: 39 | //! 40 | //! ## String Caching 41 | //! 42 | //! - You can cache strings in javascript to avoid decoding the same string multiple times. 43 | //! - If the string is static the string will be hashed by pointer instead of by value which is significantly faster. 44 | //! 45 | //! ## Byte encoded operations 46 | //! 47 | //! - Every operation is encoded as a sequence of bytes packed into an array. Every operation takes 1 byte plus whatever data is required for it. 48 | //! 49 | //! - Each operation is encoded in a batch of four as a u32. Getting a number from an array buffer has a high constant cost, but getting a u32 instead of a u8 is not more expensive. Sledgehammer bindgen reads the u32 and then splits it into the 4 individual bytes. It will shuffle and pack the bytes into as few buckets as possible and try to inline reads into the javascript. 50 | //! 51 | //! - See this benchmark: 52 | 53 | use crate::encoder::Encoder; 54 | use builder::{RustJSFlag, RustJSU32}; 55 | use encoder::Encoders; 56 | use function::FunctionBinding; 57 | use proc_macro::TokenStream; 58 | use quote::__private::{Span, TokenStream as TokenStream2}; 59 | use quote::quote; 60 | use std::{collections::BTreeSet, ops::Deref}; 61 | use syn::spanned::Spanned; 62 | use syn::{parse::Parse, parse_macro_input, Ident}; 63 | use syn::{parse_quote, Token}; 64 | use syn::{punctuated::Punctuated, Expr, Lit}; 65 | use types::string::GeneralStringFactory; 66 | 67 | mod builder; 68 | mod encoder; 69 | mod function; 70 | mod types; 71 | 72 | /// # Generates bindings for batched calls to js functions. The generated code is a Buffer struct with methods for each function. 73 | /// **The function calls to the generated methods are queued and only executed when flush is called.** 74 | /// 75 | /// Some of the code generated uses the `sledgehammer_utils` crate, so you need to add that crate as a dependency. 76 | /// 77 | /// ```rust, ignore 78 | /// #[bindgen] 79 | /// mod js { 80 | /// // You can define a struct to hold the data for the batched calls. 81 | /// struct Buffer; 82 | /// 83 | /// // JS is a special constant that defines initialization javascript. It can be used to set up the js environment and define the code that wasm-bindgen binds to. 84 | /// const JS: &str = r#" 85 | /// const text = ["hello"]; 86 | /// 87 | /// export function get(id) { 88 | /// console.log("got", text[id]); 89 | /// return text[id]; 90 | /// } 91 | /// "#; 92 | /// 93 | /// // extern blocks allow communicating with wasm-bindgen. The javascript linked is the JS constant above. 94 | /// extern "C" { 95 | /// #[wasm_bindgen] 96 | /// fn get(id: u32) -> String; 97 | /// } 98 | /// 99 | /// // valid number types are u8, u16, u32. 100 | /// fn takes_numbers(n1: u8, n2: u16, n3: u32) { 101 | /// // this is the js code that is executed when takes_numbers is called. 102 | /// // dollar signs around the arguments mark that the arguments are safe to inline (they only appear once). 103 | /// // you can escape dollar signs with a backslash. 104 | /// r#"console.log($n1$, $n2$, $n3$, "\$");"# 105 | /// } 106 | /// 107 | /// // valid string types are &str, &str, &str. 108 | /// // the generic parameter is the type of the length of the string. u32 is the default. 109 | /// fn takes_strings(str1: &str, str2: &str) { 110 | /// "console.log($str1$, $str2$);" 111 | /// } 112 | /// 113 | /// // you can also use the &str syntax to cache the string in a js variable. 114 | /// // each cache has a name that can be reused throughout the bindings so that different functions can share the same cache. 115 | /// // the cache has a size of 128 values. 116 | /// // caches on static strings use the pointer to hash the string which is faster than hashing the string itself. 117 | /// fn takes_cachable_strings(str1: &str, str2: &'static str) { 118 | /// "console.log($str1$, $str2$);" 119 | /// } 120 | /// 121 | /// // Writable allows you to pass in any type that implements the Writable trait. 122 | /// // Because all strings are encoded in a sequental buffer, every string needs to be copied to the new buffer. 123 | /// // If you only create a single string from a Arguments<'_> or number, you can use the Writable trait to avoid allocting a string and then copying it. 124 | /// // the generic parameter is the type of the length of the resulting string. u32 is the default. 125 | /// fn takes_writable(writable: impl Writable) { 126 | /// "console.log($writable$);" 127 | /// } 128 | /// 129 | /// // valid types are &[u8], &[u16], &[u32]. 130 | /// // the generic parameter is the type of the length of the array. u32 is the default. 131 | /// fn takes_slices(slice1: &[u8], slice2: &[u8]) { 132 | /// "console.log($slice1$, $slice2$);" 133 | /// } 134 | /// } 135 | /// 136 | /// let mut channel1 = Buffer::default(); 137 | /// let mut channel2 = Buffer::default(); 138 | /// channel1.takes_strings("hello", "world"); 139 | /// channel1.takes_numbers(1, 2, 3); 140 | /// channel1.takes_cachable_strings("hello", "world"); 141 | /// channel1.takes_cachable_strings("hello", "world"); 142 | /// channel1.takes_cachable_strings("hello", "world"); 143 | /// channel1.takes_writable(format_args!("hello {}", "world")); 144 | /// // append can be used to append the calls from one channel to another. 145 | /// channel2.append(channel1); 146 | /// channel2.takes_slices(&[1, 2, 3], &[4, 5, 6]); 147 | /// // flush executes all the queued calls and clears the queue. 148 | /// channel2.flush(); 149 | /// assert_eq!(get(0), "hello"); 150 | /// ``` 151 | #[proc_macro_attribute] 152 | pub fn bindgen(_: TokenStream, input: TokenStream) -> TokenStream { 153 | let mut input = parse_macro_input!(input as Bindings); 154 | 155 | input.as_tokens().into() 156 | } 157 | 158 | struct Bindings { 159 | buffer: Ident, 160 | base: String, 161 | functions: Vec, 162 | initialize: String, 163 | extends: Vec, 164 | encoders: Encoders, 165 | msg_ptr_u32: RustJSU32, 166 | msg_moved_flag: RustJSFlag, 167 | } 168 | 169 | impl Parse for Bindings { 170 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 171 | let extern_block = syn::ItemMod::parse(input)?; 172 | 173 | let mut base = String::new(); 174 | let mut buffer = None; 175 | let mut functions = Vec::new(); 176 | let mut initialize = String::new(); 177 | let mut extends = Vec::new(); 178 | let mut encoders = Encoders::default(); 179 | encoders.insert(GeneralStringFactory); 180 | for item in extern_block.content.unwrap().1 { 181 | match item { 182 | syn::Item::Fn(f) => { 183 | let f = FunctionBinding::new(&mut encoders, f)?; 184 | functions.push(f); 185 | } 186 | syn::Item::Struct(strct) => { 187 | // parse #[extends(Foo, Bar)] 188 | for attr in strct 189 | .attrs 190 | .iter() 191 | .filter(|attr| attr.path().is_ident("extends")) 192 | { 193 | let extends_classes: Punctuated = 194 | attr.parse_args_with(Punctuated::parse_separated_nonempty)?; 195 | extends.extend(extends_classes.into_iter()); 196 | } 197 | buffer = Some(strct.ident); 198 | } 199 | syn::Item::Const(cnst) => { 200 | if cnst.ident == "BASE" { 201 | let path = if let Expr::Lit(lit) = cnst.expr.deref() { 202 | if let Lit::Str(s) = &lit.lit { 203 | s.value() 204 | } else { 205 | return Err(syn::Error::new( 206 | cnst.span(), 207 | "expected string literal", 208 | )); 209 | } 210 | } else { 211 | return Err(syn::Error::new(cnst.span(), "expected string literal")); 212 | }; 213 | let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); 214 | let path = std::path::Path::new(&manifest_dir).join(path); 215 | base += &std::fs::read_to_string(&path).map_err(|e| { 216 | syn::Error::new( 217 | cnst.span(), 218 | format!( 219 | "failed to read file {} (from dir {}): {}", 220 | path.display(), 221 | manifest_dir, 222 | e 223 | ), 224 | ) 225 | })?; 226 | } 227 | } 228 | _ => return Err(syn::Error::new(item.span(), "expected function or struct")), 229 | } 230 | } 231 | 232 | for encoder in encoders.values() { 233 | initialize += &encoder.initializer(); 234 | } 235 | 236 | let msg_ptr_u32 = encoders.builder().u32(); 237 | let msg_moved_flag = encoders.builder().flag(); 238 | 239 | Ok(Bindings { 240 | buffer: buffer.unwrap_or(Ident::new("Channel", Span::call_site())), 241 | functions, 242 | extends, 243 | initialize, 244 | encoders, 245 | msg_ptr_u32, 246 | msg_moved_flag, 247 | base, 248 | }) 249 | } 250 | } 251 | 252 | fn function_discriminant_size_bits(function_count: u32) -> usize { 253 | let len = function_count + 1; 254 | let bit_size = (32 - len.next_power_of_two().leading_zeros() as usize).saturating_sub(1); 255 | match bit_size { 256 | 0..=4 => 4, 257 | 5..=8 => 8, 258 | _ => panic!("too many functions"), 259 | } 260 | } 261 | 262 | fn with_n_1_bits(n: usize) -> u32 { 263 | (1u64 << n as u64).saturating_sub(1) as u32 264 | } 265 | 266 | fn select_bits_js_inner(from: &str, size: usize, pos: usize, len: usize) -> String { 267 | if len == size { 268 | assert!(pos == 0); 269 | } 270 | assert!(len <= size); 271 | let mut s = String::new(); 272 | 273 | if pos != 0 { 274 | s += &format!("{}>>>{}", from, pos); 275 | } else { 276 | s += from; 277 | } 278 | 279 | if pos + len < size { 280 | if pos == 0 { 281 | s += &format!("&{}", with_n_1_bits(len)); 282 | } else { 283 | s = format!("({})&{}", s, with_n_1_bits(len)); 284 | } 285 | } 286 | 287 | s 288 | } 289 | 290 | impl Bindings { 291 | fn js(&mut self) -> String { 292 | let op_size = function_discriminant_size_bits(self.functions.len() as u32); 293 | let initialize = &self.initialize; 294 | 295 | let size = function_discriminant_size_bits(self.functions.len() as u32); 296 | assert!(size <= 8); 297 | let reads_per_u32 = (32 + (size - 1)) / size; 298 | 299 | let op_mask = with_n_1_bits(op_size); 300 | 301 | let match_op = self 302 | .functions 303 | .iter_mut() 304 | .enumerate() 305 | .fold(String::new(), |s, (i, f)| { 306 | s + &format!("case {}:{}break;", i, f.js()) 307 | }) 308 | + &format!("case {}:return true;", self.functions.len(),); 309 | 310 | let pre_run_js = self 311 | .encoders 312 | .values() 313 | .fold(String::new(), |s, e| s + &e.pre_run_js()); 314 | 315 | let msg_ptr_moved = self.msg_moved_flag.read_js(); 316 | let read_msg_ptr = self.msg_ptr_u32.read_js(); 317 | 318 | let pre_run_metadata = self.encoders.builder().pre_run_js(); 319 | 320 | let all_variables: BTreeSet<&str> = self 321 | .functions 322 | .iter() 323 | .flat_map(|f| f.variables.iter().map(|s| s.as_str())) 324 | .collect(); 325 | 326 | let declarations = if all_variables.is_empty() { 327 | String::new() 328 | } else { 329 | let mut all_variables_string = String::from("let "); 330 | for var in all_variables { 331 | all_variables_string += var; 332 | all_variables_string += ","; 333 | } 334 | all_variables_string.pop(); 335 | all_variables_string += ";"; 336 | all_variables_string 337 | }; 338 | 339 | let extends = if !self.extends.is_empty() { 340 | let comma_separated_classes = self 341 | .extends 342 | .iter() 343 | .map(|i| i.to_string()) 344 | .collect::>() 345 | .join(","); 346 | "extends ".to_string() + &comma_separated_classes 347 | } else { 348 | String::new() 349 | }; 350 | let super_constructor = if !self.extends.is_empty() { 351 | "super();" 352 | } else { 353 | "" 354 | }; 355 | let class_name = self.buffer.to_string(); 356 | let base = &self.base; 357 | 358 | let js = format!( 359 | r#" 360 | {base} 361 | {declarations} 362 | export class Raw{class_name} {extends} {{ 363 | constructor(r) {{ 364 | {super_constructor} 365 | this.d=r; 366 | this.m = null; 367 | this.p = null; 368 | this.ls = null; 369 | this.t = null; 370 | this.op = null; 371 | this.e = null; 372 | this.z = null; 373 | this.metaflags = null; 374 | {initialize} 375 | }} 376 | 377 | update_memory(b){{ 378 | this.m=new DataView(b.buffer) 379 | }} 380 | 381 | run(){{ 382 | {pre_run_metadata} 383 | if({msg_ptr_moved}){{ 384 | this.ls={read_msg_ptr}; 385 | }} 386 | this.p=this.ls; 387 | {pre_run_js} 388 | for(;;){{ 389 | this.op=this.m.getUint32(this.p,true); 390 | this.p+=4; 391 | this.z=0; 392 | while(this.z++<{reads_per_u32}){{ 393 | switch(this.op&{op_mask}){{ 394 | {match_op} 395 | }} 396 | this.op>>>={op_size}; 397 | }} 398 | }} 399 | }} 400 | 401 | run_from_bytes(bytes){{ 402 | this.d = 0; 403 | this.update_memory(new Uint8Array(bytes)) 404 | this.run() 405 | }} 406 | }}"#, 407 | ); 408 | 409 | js 410 | } 411 | 412 | fn as_tokens(&mut self) -> TokenStream2 { 413 | let all_js = self.js(); 414 | let channel = self.channel(); 415 | 416 | let web_entrypoint = { 417 | #[cfg(feature = "web")] 418 | { 419 | let ty = &self.buffer; 420 | let extends = if !self.extends.is_empty() { 421 | let classes = &self.extends; 422 | quote! { 423 | #[wasm_bindgen(extends = #(#classes),*)] 424 | } 425 | } else { 426 | quote!() 427 | }; 428 | let raw_type = &Ident::new(&format!("Raw{}", ty), Span::call_site()); 429 | quote! { 430 | #[::sledgehammer_bindgen::wasm_bindgen::prelude::wasm_bindgen(inline_js = #all_js)] 431 | extern "C" { 432 | #extends 433 | pub type #raw_type; 434 | 435 | #[wasm_bindgen(constructor)] 436 | fn new(metadata_ptr: u32) -> #raw_type; 437 | 438 | #[wasm_bindgen(method)] 439 | fn run(this: &#raw_type); 440 | 441 | #[wasm_bindgen(method)] 442 | #[doc = concat!("Runs the serialized message provided")] 443 | #[doc = concat!("To create a serialized message, use the [`", stringify!(#ty), "`]::to_bytes` method")] 444 | pub fn run_from_buffer(this: &#raw_type, buffer: &[u8]); 445 | 446 | #[wasm_bindgen(method)] 447 | fn update_memory(this: &#raw_type, memory: ::sledgehammer_bindgen::wasm_bindgen::JsValue); 448 | } 449 | } 450 | } 451 | #[cfg(not(feature = "web"))] 452 | { 453 | quote!() 454 | } 455 | }; 456 | 457 | quote! { 458 | #web_entrypoint 459 | #channel 460 | const GENERATED_JS: &str = #all_js; 461 | } 462 | } 463 | 464 | fn channel(&mut self) -> TokenStream2 { 465 | let methods = self 466 | .functions 467 | .iter() 468 | .enumerate() 469 | .map(|(i, f)| f.to_tokens(i as u8)); 470 | let end_msg = self.functions.len() as u8; 471 | let no_op = self.functions.len() as u8 + 1; 472 | let size = function_discriminant_size_bits(self.functions.len() as u32); 473 | let reads_per_u32 = (32 + (size - 1)) / size; 474 | let encode_op = match size { 475 | 4 => { 476 | quote! { 477 | if self.current_op_byte_idx % 2 == 0 { 478 | *self.msg.get_unchecked_mut(self.current_op_batch_idx + self.current_op_byte_idx / 2) = op; 479 | } else { 480 | *self.msg.get_unchecked_mut(self.current_op_batch_idx + self.current_op_byte_idx / 2) |= op << 4; 481 | } 482 | self.current_op_byte_idx += 1; 483 | } 484 | } 485 | 8 => { 486 | quote! { 487 | *self.msg.get_unchecked_mut(self.current_op_batch_idx + self.current_op_byte_idx) = op; 488 | self.current_op_byte_idx += 1; 489 | } 490 | } 491 | _ => panic!("unsupported size"), 492 | }; 493 | 494 | let ty = &self.buffer; 495 | let states = self.encoders.iter().map(|(_, e)| { 496 | let ty = &e.rust_type(); 497 | let ident = &e.rust_ident(); 498 | quote! { 499 | #ident: #ty, 500 | } 501 | }); 502 | let states_default = self.encoders.iter().map(|(_, e)| { 503 | let ident = &e.rust_ident(); 504 | quote! { 505 | #ident: Default::default(), 506 | } 507 | }); 508 | 509 | let pre_run_rust = self 510 | .encoders 511 | .iter() 512 | .map(|(_, e)| e.pre_run_rust()) 513 | .collect::>(); 514 | let memory_moved_rust = self 515 | .encoders 516 | .iter() 517 | .map(|(_, e)| e.memory_moved_rust()) 518 | .collect::>(); 519 | let first_run_states = self 520 | .encoders 521 | .iter() 522 | .map(|(_, e)| e.init_rust()) 523 | .collect::>(); 524 | let post_run_rust = self 525 | .encoders 526 | .iter() 527 | .map(|(_, e)| e.post_run_rust()) 528 | .collect::>(); 529 | 530 | let export_memory_iters = self 531 | .encoders 532 | .iter() 533 | .map(|(ident, e)| { 534 | // Merge the memory of the #ident encoder 535 | let comment = quote!( 536 | #[doc = concat!(" The memory of the [`", stringify!(#ident), "`] encoder")] 537 | ); 538 | 539 | let merge = e.merge_memory_rust(); 540 | quote! { 541 | #comment 542 | #merge 543 | } 544 | }) 545 | .collect::>(); 546 | 547 | let meta_type = self.encoders.builder.rust_type(); 548 | let meta_ident = self.encoders.builder.rust_ident(); 549 | let meta_init = self.encoders.builder.rust_init(); 550 | 551 | let set_msg_ptr = self 552 | .msg_ptr_u32 553 | .write_rust(parse_quote! {self.msg.as_ptr() as u32}); 554 | let set_exported_msg_ptr = self.msg_ptr_u32.write_rust(parse_quote! {current_ptr}); 555 | let set_msg_moved = self.msg_moved_flag.write_rust(parse_quote! {msg_moved}); 556 | let get_msg_ptr = self.msg_ptr_u32.get_rust(); 557 | let raw_type = &Ident::new(&format!("Raw{}", ty), Span::call_site()); 558 | 559 | let js_channel_field = if cfg!(feature = "web") { 560 | quote! { 561 | #[cfg(target_family = "wasm")] 562 | js_channel: #raw_type, 563 | } 564 | } else { 565 | quote!() 566 | }; 567 | 568 | let js_channel_init = if cfg!(feature = "web") { 569 | quote! { 570 | #[cfg(target_family = "wasm")] 571 | // SAFETY: self.metadata is pinned, initialized and we will not write to it while javascript is reading from it 572 | js_channel: #raw_type::new(#meta_ident.as_ref().get_ref() as *const _ as u32), 573 | } 574 | } else { 575 | quote!() 576 | }; 577 | 578 | let js_channel_getter = if cfg!(feature = "web") { 579 | quote! { 580 | pub fn js_channel(&self) -> &#raw_type { 581 | #[cfg(target_family = "wasm")] 582 | { 583 | &self.js_channel 584 | } 585 | #[cfg(not(target_family = "wasm"))] 586 | { 587 | panic!("js_channel is only available in wasm") 588 | } 589 | } 590 | } 591 | } else { 592 | quote!() 593 | }; 594 | 595 | quote! { 596 | fn __copy(src: &[u8], dst: &mut [u8], len: usize) { 597 | for (m, i) in dst.iter_mut().zip(src.iter().take(len)) { 598 | *m = *i; 599 | } 600 | } 601 | 602 | pub struct #ty { 603 | msg: Vec, 604 | current_op_batch_idx: usize, 605 | current_op_byte_idx: usize, 606 | last_mem_size: usize, 607 | #meta_ident: #meta_type, 608 | #js_channel_field 609 | first_run: bool, 610 | #( #states )* 611 | } 612 | 613 | impl Default for #ty { 614 | fn default() -> Self { 615 | let #meta_ident: #meta_type = #meta_init; 616 | Self { 617 | msg: Vec::new(), 618 | current_op_batch_idx: 0, 619 | last_mem_size: 0, 620 | current_op_byte_idx: #reads_per_u32, 621 | #js_channel_init 622 | first_run: true, 623 | #meta_ident, 624 | #( #states_default )* 625 | } 626 | } 627 | } 628 | 629 | impl #ty { 630 | pub fn append(&mut self, mut batch: Self) { 631 | // add empty operations to the batch to make sure the batch is aligned 632 | let operations_left = #reads_per_u32 - self.current_op_byte_idx; 633 | for _ in 0..operations_left { 634 | self.encode_op(#no_op); 635 | } 636 | 637 | self.current_op_byte_idx = batch.current_op_byte_idx; 638 | self.current_op_batch_idx = self.msg.len() + batch.current_op_batch_idx; 639 | self.msg.append(&mut batch.msg); 640 | } 641 | 642 | #[allow(clippy::uninit_vec)] 643 | fn encode_op(&mut self, op: u8) { 644 | unsafe { 645 | // SAFETY: this creates 4 bytes of uninitialized memory that will be immediately written to when we encode the operation in the next step 646 | if self.current_op_byte_idx >= #reads_per_u32 { 647 | self.current_op_batch_idx = self.msg.len(); 648 | self.msg.reserve(4); 649 | self.msg.set_len(self.msg.len() + 4); 650 | self.current_op_byte_idx = 0; 651 | } 652 | // SAFETY: we just have checked that there is enough space in the vector to index into it 653 | #encode_op 654 | } 655 | } 656 | 657 | pub fn flush(&mut self){ 658 | #[cfg(target_family = "wasm")] 659 | { 660 | self.encode_op(#end_msg); 661 | #set_msg_ptr 662 | self.update_metadata_ptrs(); 663 | #(#pre_run_rust)* 664 | 665 | let new_mem_size = core::arch::wasm32::memory_size(0); 666 | // we need to update the memory if the memory has grown 667 | if new_mem_size != self.last_mem_size { 668 | self.last_mem_size = new_mem_size; 669 | self.js_channel.update_memory(::sledgehammer_bindgen::wasm_bindgen::memory()); 670 | #(#memory_moved_rust)* 671 | } 672 | 673 | self.js_channel.run(); 674 | 675 | self.reset(); 676 | } 677 | } 678 | 679 | pub fn export_memory(&mut self) -> impl Iterator + '_ { 680 | self.encode_op(#end_msg); 681 | #(#pre_run_rust)* 682 | 683 | #(#memory_moved_rust)* 684 | let msg_moved = true; 685 | #set_msg_moved 686 | 687 | let msg = &self.msg; 688 | let meta = &self.#meta_ident; 689 | 690 | let meta_iter = meta.iter().flat_map(|i| i.get().to_le_bytes().into_iter()); 691 | let mut current_ptr = meta.len() as u32 * 4; 692 | #set_exported_msg_ptr 693 | let iter = msg.iter().copied(); 694 | current_ptr += msg.len() as u32; 695 | #( 696 | let iter = iter.chain(#export_memory_iters); 697 | )* 698 | 699 | meta_iter.chain(iter) 700 | } 701 | 702 | pub fn reset(&mut self){ 703 | #(#post_run_rust)* 704 | self.current_op_batch_idx = 0; 705 | self.current_op_byte_idx = #reads_per_u32; 706 | self.msg.clear(); 707 | } 708 | 709 | #js_channel_getter 710 | 711 | fn update_metadata_ptrs(&mut self) { 712 | let first_run = self.first_run; 713 | self.first_run = false; 714 | let metadata_ptr = self.metadata.as_ref().get_ref() as *const _ as u32; 715 | 716 | // the pointer will only be updated when the message vec is resized, so we have a flag to check if the pointer has changed to avoid unnecessary decoding 717 | if first_run { 718 | #(#first_run_states)* 719 | // this is the first message, so we need to encode all the metadata 720 | #set_msg_ptr 721 | let msg_moved = true; 722 | #set_msg_moved 723 | } else { 724 | let msg_moved = #get_msg_ptr != metadata_ptr; 725 | if msg_moved { 726 | #set_msg_ptr 727 | } 728 | #set_msg_moved 729 | } 730 | } 731 | 732 | #(#methods)* 733 | } 734 | } 735 | } 736 | } 737 | -------------------------------------------------------------------------------- /sledgehammer_bindgen_macro/src/types/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod numbers; 2 | pub mod slice; 3 | pub mod string; 4 | pub mod writable; 5 | -------------------------------------------------------------------------------- /sledgehammer_bindgen_macro/src/types/numbers.rs: -------------------------------------------------------------------------------- 1 | use quote::__private::TokenStream as TokenStream2; 2 | use quote::quote; 3 | use syn::parse_quote; 4 | use syn::Ident; 5 | use syn::Type; 6 | 7 | use crate::builder::{RustJSFlag, RustJSU32}; 8 | use crate::encoder::*; 9 | 10 | pub struct NumberEncoder { 11 | array_moved_flag: RustJSFlag, 12 | array_ptr: RustJSU32, 13 | } 14 | 15 | impl NumberEncoder { 16 | pub fn pointer_js(&self) -> String { 17 | let size = self.size(); 18 | format!("this.u{size}bufp") 19 | } 20 | 21 | pub fn size(&self) -> u32 { 22 | match S { 23 | 1 => 8, 24 | 2 => 16, 25 | 4 => 32, 26 | _ => panic!("Invalid number size"), 27 | } 28 | } 29 | 30 | pub fn element_type(&self) -> Type { 31 | match S { 32 | 1 => parse_quote! {u8}, 33 | 2 => parse_quote! {u16}, 34 | 4 => parse_quote! {u32}, 35 | _ => panic!("Invalid number size"), 36 | } 37 | } 38 | } 39 | 40 | pub struct NumberEncoderFactory; 41 | 42 | impl CreateEncoder for NumberEncoderFactory { 43 | type Output = NumberEncoder; 44 | 45 | fn create(&self, encoders: &mut Encoders) -> Self::Output { 46 | let builder = encoders.builder(); 47 | let array_moved_flag = builder.flag(); 48 | let array_ptr = builder.u32(); 49 | NumberEncoder { 50 | array_moved_flag, 51 | array_ptr, 52 | } 53 | } 54 | 55 | fn rust_ident(&self) -> Ident { 56 | match S { 57 | 1 => parse_quote! {U8}, 58 | 2 => parse_quote! {U16}, 59 | 4 => parse_quote! {U32}, 60 | _ => panic!("Invalid number size"), 61 | } 62 | } 63 | } 64 | 65 | impl Encoder for NumberEncoder { 66 | fn initializer(&self) -> String { 67 | let size = self.size(); 68 | format!("this.u{size}buf=null;this.u{size}bufp=null;") 69 | } 70 | 71 | fn pre_run_js(&self) -> String { 72 | let moved = self.array_moved_flag.read_js(); 73 | let ptr = self.array_ptr.read_js(); 74 | let size = self.size(); 75 | let size_in_bytes = size / 8; 76 | let pointer = self.pointer_js(); 77 | format!( 78 | "if ({moved}){{ 79 | this.t = {ptr}; 80 | this.u{size}buf=new Uint{size}Array(this.m.buffer,this.t,((this.m.buffer.byteLength-this.t)-(this.m.buffer.byteLength-this.t)%{size_in_bytes})/{size_in_bytes}); 81 | }} 82 | {pointer}=0;" 83 | ) 84 | } 85 | 86 | fn rust_type(&self) -> Type { 87 | match S { 88 | 1 => parse_quote! {Vec}, 89 | 2 => parse_quote! {Vec}, 90 | 4 => parse_quote! {Vec}, 91 | _ => panic!("Invalid number size"), 92 | } 93 | } 94 | 95 | fn rust_ident(&self) -> Ident { 96 | match S { 97 | 1 => parse_quote! {u8_arr}, 98 | 2 => parse_quote! {u16_arr}, 99 | 4 => parse_quote! {u32_arr}, 100 | _ => panic!("Invalid number size"), 101 | } 102 | } 103 | 104 | fn init_rust(&self) -> TokenStream2 { 105 | self.array_moved_flag.write_rust(parse_quote!(true)) 106 | } 107 | 108 | fn memory_moved_rust(&self) -> TokenStream2 { 109 | self.init_rust() 110 | } 111 | 112 | fn pre_run_rust(&self) -> TokenStream2 { 113 | let ident = self.rust_ident(); 114 | let write_ptr = self 115 | .array_ptr 116 | .write_rust(parse_quote!(self.#ident.as_ptr() as u32)); 117 | let read_ptr = self.array_ptr.get_rust(); 118 | let moved = self 119 | .array_moved_flag 120 | .write_rust(parse_quote!(#read_ptr != self.#ident.as_ptr() as u32)); 121 | 122 | quote! { 123 | #moved 124 | #write_ptr 125 | } 126 | } 127 | 128 | fn post_run_rust(&self) -> TokenStream2 { 129 | let ident = self.rust_ident(); 130 | quote! { 131 | self.#ident.clear(); 132 | } 133 | } 134 | 135 | fn merge_memory_rust(&self) -> TokenStream2 { 136 | let ident = self.rust_ident(); 137 | let write_ptr = self.array_ptr.write_rust(parse_quote!(current_ptr)); 138 | let add_buffer = (S != 1).then(|| { 139 | quote! { 140 | let buffer_size = (#S - current_ptr % #S); 141 | let zeroed_buffer = std::iter::repeat(0u8).take(buffer_size as usize); 142 | current_ptr += buffer_size; 143 | } 144 | }); 145 | let buffer_memory = 146 | quote! { self.#ident.iter().flat_map(|&x| x.to_le_bytes().into_iter()) }; 147 | let final_memory = if S != 1 { 148 | quote! { 149 | zeroed_buffer.chain(#buffer_memory) 150 | } 151 | } else { 152 | buffer_memory 153 | }; 154 | quote! { 155 | // align the array pointer so that it is a multiple of N 156 | { 157 | #add_buffer 158 | #write_ptr 159 | current_ptr += self.#ident.len() as u32 * #S; 160 | #final_memory 161 | } 162 | } 163 | } 164 | } 165 | 166 | impl NumberEncoder { 167 | pub(crate) fn js_ident(&self) -> String { 168 | let size = self.size(); 169 | format!("this.u{size}buf") 170 | } 171 | } 172 | 173 | impl Encode for NumberEncoder { 174 | fn encode_js(&self) -> String { 175 | let size = self.size(); 176 | let pointer = self.pointer_js(); 177 | format!("this.u{size}buf[{pointer}++]") 178 | } 179 | 180 | fn encode_rust(&self, ident: &Ident) -> TokenStream2 { 181 | let rust_ident = self.rust_ident(); 182 | quote! { 183 | self.#rust_ident.push(#ident); 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /sledgehammer_bindgen_macro/src/types/slice.rs: -------------------------------------------------------------------------------- 1 | use super::numbers::{NumberEncoder, NumberEncoderFactory}; 2 | use crate::encoder::{CreateEncoder, Encode, EncodeTraitObject, Encoder, Encoders}; 3 | 4 | use quote::__private::{Span, TokenStream as TokenStream2}; 5 | use quote::quote; 6 | use syn::parse_quote; 7 | use syn::{Ident, Type}; 8 | 9 | pub struct SliceFactory; 10 | 11 | impl CreateEncoder for SliceFactory { 12 | type Output = Slice; 13 | 14 | fn create(&self, encoder: &mut Encoders) -> Self::Output { 15 | let array = encoder 16 | .get_or_insert_with(NumberEncoderFactory::) 17 | .clone(); 18 | let len = encoder 19 | .get_or_insert_with(NumberEncoderFactory::) 20 | .clone(); 21 | let ptr = encoder 22 | .get_or_insert_with(NumberEncoderFactory::<4>) 23 | .clone(); 24 | Slice { array, len, ptr } 25 | } 26 | 27 | fn rust_ident(&self) -> Ident { 28 | Ident::new(format!("slice{}", N).as_str(), Span::call_site()) 29 | } 30 | } 31 | 32 | pub struct Slice { 33 | array: EncodeTraitObject, 34 | len: EncodeTraitObject, 35 | ptr: EncodeTraitObject, 36 | } 37 | 38 | impl Slice { 39 | fn array(&self) -> &NumberEncoder { 40 | self.array.downcast() 41 | } 42 | } 43 | 44 | impl Encoder for Slice { 45 | fn rust_type(&self) -> Type { 46 | parse_quote!(()) 47 | } 48 | 49 | fn rust_ident(&self) -> Ident { 50 | Ident::new(format!("slice{}", N).as_str(), Span::call_site()) 51 | } 52 | } 53 | 54 | impl Encode for Slice { 55 | fn encode_js(&self) -> String { 56 | let ptr_read = self.ptr.encode_js(); 57 | let len_read = self.len.encode_js(); 58 | if STATIC { 59 | match N { 60 | 1 => format!("new Uint8Array(this.m.buffer,{},{})", ptr_read, len_read), 61 | 2 => format!("new Uint16Array(this.m.buffer,{},{})", ptr_read, len_read), 62 | 4 => format!("new Uint32Array(this.m.buffer,{},{})", ptr_read, len_read), 63 | _ => todo!(), 64 | } 65 | } else { 66 | let array_ptr = self.array().pointer_js(); 67 | let array_read = self.array().js_ident(); 68 | let slice_end = format!("{array_ptr}+{len_read}"); 69 | format!("(()=>{{this.e={slice_end};const final_array = {array_read}.slice({array_ptr},this.e);{array_ptr}=this.e;return final_array;}})()") 70 | } 71 | } 72 | 73 | fn encode_rust(&self, ident: &Ident) -> TokenStream2 { 74 | let len = Ident::new("__len", Span::call_site()); 75 | let encode_len = self.len.encode_rust(&len); 76 | let ptr = Ident::new("__ptr", Span::call_site()); 77 | let encode_ptr = self.ptr.encode_rust(&ptr); 78 | let write_array = (!STATIC).then(|| { 79 | let item = Ident::new("_i", Span::call_site()); 80 | let encode_array = self.array.encode_rust(&item); 81 | quote! { 82 | for &#item in #ident { 83 | #encode_array 84 | } 85 | } 86 | }); 87 | let write_ptr = match STATIC { 88 | true => quote! { 89 | let #ptr = #ident.as_ptr() as u32; 90 | #encode_ptr 91 | }, 92 | false => { 93 | quote! {} 94 | } 95 | }; 96 | quote! { 97 | #write_ptr 98 | let #len = #ident.len() as u32; 99 | #encode_len 100 | #write_array 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /sledgehammer_bindgen_macro/src/types/string.rs: -------------------------------------------------------------------------------- 1 | use quote::__private::{Span, TokenStream as TokenStream2}; 2 | use quote::quote; 3 | use syn::{parse_quote, Ident, Type}; 4 | 5 | use crate::builder::{RustJSFlag, RustJSU32}; 6 | use crate::encoder::{CreateEncoder, Encode, EncodeTraitObject, Encoder, Encoders}; 7 | 8 | use super::numbers::{NumberEncoder, NumberEncoderFactory}; 9 | 10 | const CACHE_MISS_BIT: u32 = 1 << 7; 11 | 12 | pub struct GeneralStringFactory; 13 | 14 | pub struct GeneralString { 15 | str_moved_flag: RustJSFlag, 16 | str_tiny_flag: RustJSFlag, 17 | str_used_flag: RustJSFlag, 18 | str_ptr: RustJSU32, 19 | len: RustJSU32, 20 | } 21 | 22 | impl CreateEncoder for GeneralStringFactory { 23 | type Output = GeneralString; 24 | 25 | fn create(&self, encoder: &mut Encoders) -> GeneralString { 26 | let builder = encoder.builder(); 27 | let str_moved_flag = builder.flag(); 28 | let str_tiny_flag = builder.flag(); 29 | let str_used_flag = builder.flag(); 30 | let str_ptr = builder.u32(); 31 | let len = builder.u32(); 32 | GeneralString { 33 | str_moved_flag, 34 | str_tiny_flag, 35 | str_used_flag, 36 | str_ptr, 37 | len, 38 | } 39 | } 40 | 41 | fn rust_ident(&self) -> Ident { 42 | parse_quote!(str_buffer) 43 | } 44 | } 45 | 46 | impl Encode for GeneralString { 47 | fn encode_js(&self) -> String { 48 | todo!() 49 | } 50 | 51 | fn encode_rust(&self, _: &Ident) -> TokenStream2 { 52 | todo!() 53 | } 54 | } 55 | 56 | impl Encoder for GeneralString { 57 | fn rust_type(&self) -> Type { 58 | parse_quote! {Vec} 59 | } 60 | 61 | fn rust_ident(&self) -> Ident { 62 | parse_quote! {str_buffer} 63 | } 64 | 65 | fn initializer(&self) -> String { 66 | "this.s = \"\";this.lsp = null;this.sp = null;this.sl = null;this.c = new TextDecoder();" 67 | .to_string() 68 | } 69 | 70 | fn pre_run_js(&self) -> String { 71 | let moved = self.str_moved_flag.read_js(); 72 | let used = self.str_used_flag.read_js(); 73 | let tiny = self.str_tiny_flag.read_js(); 74 | let ptr = self.str_ptr.read_js(); 75 | let len = self.len.read_js(); 76 | format!( 77 | r#"if ({moved}){{ 78 | this.lsp = {ptr}; 79 | }} 80 | if ({used}) {{ 81 | this.sl = {len}; 82 | if ({tiny}) {{ 83 | this.sp = this.lsp; 84 | this.s = ""; 85 | this.e = this.sp + ((this.sl / 4) | 0) * 4; 86 | while (this.sp < this.e) {{ 87 | this.t = this.m.getUint32(this.sp, true); 88 | this.s += String.fromCharCode( 89 | this.t & 255, 90 | (this.t & 65280) >> 8, 91 | (this.t & 16711680) >> 16, 92 | this.t >> 24 93 | ); 94 | this.sp += 4; 95 | }} 96 | while (this.sp < this.lsp + this.sl) {{ 97 | this.s += String.fromCharCode(this.m.getUint8(this.sp++)); 98 | }} 99 | }} else {{ 100 | let buffer = new Uint8Array(this.m.buffer, this.lsp, this.sl); 101 | // If the wasm buffer is a shared array buffer, we need to copy the data out before decoding https://github.com/DioxusLabs/dioxus/issues/2589 102 | // Note: We intentionally don't use instanceof here because SharedArrayBuffer can be created even when SharedArrayBuffer is not defined... 103 | if (this.m.buffer.constructor.name === "SharedArrayBuffer") {{ 104 | let arrayBuffer = new ArrayBuffer(this.sl); 105 | new Uint8Array(arrayBuffer).set(buffer); 106 | buffer = arrayBuffer; 107 | }} 108 | this.s = this.c.decode(buffer); 109 | }} 110 | }} 111 | this.sp=0;"# 112 | ) 113 | } 114 | 115 | fn init_rust(&self) -> TokenStream2 { 116 | self.str_moved_flag.write_rust(parse_quote!(true)) 117 | } 118 | 119 | fn memory_moved_rust(&self) -> TokenStream2 { 120 | self.init_rust() 121 | } 122 | 123 | fn pre_run_rust(&self) -> TokenStream2 { 124 | let ident = ::rust_ident(self); 125 | let write_ptr = self 126 | .str_ptr 127 | .write_rust(parse_quote!(self.#ident.as_ptr() as u32)); 128 | let write_small = self 129 | .str_tiny_flag 130 | .write_rust(parse_quote!(self.#ident.len() < 100 && self.str_buffer.is_ascii())); 131 | let write_used = self 132 | .str_used_flag 133 | .write_rust(parse_quote!(!self.str_buffer.is_empty())); 134 | let len = self.len.write_rust(parse_quote!(self.#ident.len() as u32)); 135 | let read_ptr = self.str_ptr.get_rust(); 136 | let moved = self 137 | .str_moved_flag 138 | .write_rust(parse_quote!(#read_ptr != self.#ident.as_ptr() as u32)); 139 | 140 | quote! { 141 | if !self.str_buffer.is_empty() { 142 | #moved 143 | #write_small 144 | #write_ptr 145 | #len 146 | } 147 | #write_used 148 | } 149 | } 150 | 151 | fn post_run_rust(&self) -> TokenStream2 { 152 | let ident = ::rust_ident(self); 153 | quote! { 154 | self.#ident.clear(); 155 | } 156 | } 157 | 158 | fn merge_memory_rust(&self) -> TokenStream2 { 159 | let ident = ::rust_ident(self); 160 | let write_ptr = self.str_ptr.write_rust(parse_quote!(current_ptr)); 161 | quote! { 162 | { 163 | #write_ptr 164 | current_ptr += self.#ident.len() as u32; 165 | self.#ident.iter().copied() 166 | } 167 | } 168 | } 169 | } 170 | 171 | pub struct StrEncoder { 172 | size_type: EncodeTraitObject, 173 | cache_name: Option<(Ident, EncodeTraitObject)>, 174 | static_str: bool, 175 | } 176 | 177 | impl StrEncoder { 178 | fn size_type(&self) -> &NumberEncoder { 179 | self.size_type.downcast() 180 | } 181 | } 182 | 183 | pub struct StrEncoderFactory { 184 | pub cache_name: Option, 185 | pub static_str: bool, 186 | } 187 | 188 | impl CreateEncoder for StrEncoderFactory { 189 | type Output = StrEncoder; 190 | 191 | fn create(&self, encoder: &mut Encoders) -> Self::Output { 192 | StrEncoder { 193 | size_type: encoder.get_or_insert_with(NumberEncoderFactory::), 194 | cache_name: self 195 | .cache_name 196 | .clone() 197 | .map(|name| (name, encoder.get_or_insert_with(NumberEncoderFactory::<1>))), 198 | static_str: self.static_str, 199 | } 200 | } 201 | 202 | fn rust_ident(&self) -> Ident { 203 | if let Some(cache) = &self.cache_name { 204 | cache.clone() 205 | } else { 206 | Ident::new(&format!("str_cache{}", S * 8), Span::call_site()) 207 | } 208 | } 209 | } 210 | 211 | impl Encoder for StrEncoder { 212 | fn rust_type(&self) -> Type { 213 | if self.cache_name.is_some() { 214 | if self.static_str { 215 | parse_quote! { 216 | sledgehammer_utils::PtrConstLru<128, 256> 217 | } 218 | } else { 219 | parse_quote! { 220 | sledgehammer_utils::FxConstLru 221 | } 222 | } 223 | } else { 224 | parse_quote! {()} 225 | } 226 | } 227 | 228 | fn rust_ident(&self) -> Ident { 229 | if let Some((cache, _)) = &self.cache_name { 230 | cache.clone() 231 | } else { 232 | Ident::new(&format!("str_cache{}", S * 8), Span::call_site()) 233 | } 234 | } 235 | 236 | fn initializer(&self) -> String { 237 | match &self.cache_name { 238 | Some((cache, encoder)) => { 239 | let get_u8 = encoder.encode_js(); 240 | 241 | let cache_idx_mask = !CACHE_MISS_BIT; 242 | let read_string_length = self.size_type.encode_js(); 243 | format!( 244 | "this.{cache} = []; 245 | this.{cache}_cache_hit = null; 246 | this.{cache}_cache_idx; 247 | this.get_{cache} = function() {{ 248 | this.{cache}_cache_idx = {get_u8}; 249 | if(this.{cache}_cache_idx & {CACHE_MISS_BIT}){{ 250 | this.{cache}_cache_hit=this.s.substring(this.sp,this.sp+={read_string_length}); 251 | this.{cache}[this.{cache}_cache_idx&{cache_idx_mask}]=this.{cache}_cache_hit; 252 | return this.{cache}_cache_hit; 253 | }} 254 | else{{ 255 | return this.{cache}[this.{cache}_cache_idx&{cache_idx_mask}]; 256 | }} 257 | }};", 258 | ) 259 | } 260 | None => "".to_string(), 261 | } 262 | } 263 | } 264 | 265 | impl Encode for StrEncoder { 266 | fn encode_js(&self) -> String { 267 | match &self.cache_name { 268 | Some((cache, _)) => format!("this.get_{cache}()"), 269 | None => format!( 270 | "this.s.substring(this.sp,this.sp+={})", 271 | self.size_type.encode_js() 272 | ), 273 | } 274 | } 275 | 276 | fn encode_rust(&self, name: &Ident) -> TokenStream2 { 277 | let len = Ident::new("__len", Span::call_site()); 278 | let char_len = Ident::new("__char_len", Span::call_site()); 279 | let write_len = self.size_type.encode_rust(&char_len); 280 | let char_len_type = self.size_type().element_type(); 281 | let encode = quote! { 282 | let #len = #name.len(); 283 | let #char_len: usize = #name.chars().map(|c| c.len_utf16()).sum(); 284 | let #char_len = { 285 | #char_len as #char_len_type 286 | }; 287 | #write_len 288 | let old_len = self.str_buffer.len(); 289 | unsafe { 290 | // SAFETY: We reserve uninitialized memory but then immediately write to it to make it initialized 291 | self.str_buffer.reserve(#len); 292 | self.str_buffer.set_len(old_len + #len); 293 | __copy(#name.as_bytes(), &mut self.str_buffer[old_len..], #len); 294 | } 295 | }; 296 | match &self.cache_name { 297 | Some((cache, size)) => { 298 | let write_size = size.encode_rust(&Ident::new("cache_id", Span::call_site())); 299 | let push = if self.static_str { 300 | quote! { 301 | self.#cache.push(&(sledgehammer_utils::StaticPtr::from(#name))) 302 | } 303 | } else { 304 | quote! { 305 | self.#cache.push(#name) 306 | } 307 | }; 308 | quote! { 309 | let (_id, _new) = #push; 310 | if _new { 311 | let cache_id = #CACHE_MISS_BIT as u8 | _id; 312 | #write_size 313 | #encode 314 | } 315 | else { 316 | let cache_id = _id; 317 | #write_size 318 | } 319 | } 320 | } 321 | None => encode, 322 | } 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /sledgehammer_bindgen_macro/src/types/writable.rs: -------------------------------------------------------------------------------- 1 | use quote::__private::{Span, TokenStream as TokenStream2}; 2 | use quote::quote; 3 | use syn::{parse_quote, Ident, Type}; 4 | 5 | use crate::encoder::{CreateEncoder, Encode, EncodeTraitObject, Encoder, Encoders}; 6 | 7 | use super::numbers::{NumberEncoder, NumberEncoderFactory}; 8 | 9 | pub struct WritableEncoder { 10 | size_type: EncodeTraitObject, 11 | } 12 | 13 | pub struct WritableEncoderFactory; 14 | 15 | impl CreateEncoder for WritableEncoderFactory { 16 | type Output = WritableEncoder; 17 | 18 | fn create(&self, builder: &mut Encoders) -> Self::Output { 19 | WritableEncoder { 20 | size_type: builder 21 | .get_or_insert_with(NumberEncoderFactory::) 22 | .clone(), 23 | } 24 | } 25 | 26 | fn rust_ident(&self) -> Ident { 27 | Ident::new(&format!("writable_{}", S * 8), Span::call_site()) 28 | } 29 | } 30 | 31 | impl WritableEncoder { 32 | fn size_type(&self) -> &NumberEncoder { 33 | self.size_type.downcast() 34 | } 35 | } 36 | 37 | impl Encoder for WritableEncoder { 38 | fn rust_type(&self) -> Type { 39 | parse_quote! {()} 40 | } 41 | 42 | fn rust_ident(&self) -> Ident { 43 | Ident::new(&format!("writable_{}", S * 8), Span::call_site()) 44 | } 45 | } 46 | 47 | impl Encode for WritableEncoder { 48 | fn encode_js(&self) -> String { 49 | format!( 50 | "this.s.substring(this.sp,this.sp+={})", 51 | self.size_type.encode_js() 52 | ) 53 | } 54 | 55 | fn encode_rust(&self, name: &Ident) -> TokenStream2 { 56 | let char_len = Ident::new("char_len", Span::call_site()); 57 | let write_len = self.size_type.encode_rust(&char_len); 58 | let char_len_type = self.size_type().element_type(); 59 | quote! { 60 | let prev_len = self.str_buffer.len(); 61 | #name.write(&mut self.str_buffer); 62 | // the length of the string is the change in length of the string buffer 63 | let #char_len: usize = unsafe { std::str::from_utf8_unchecked(&self.str_buffer[prev_len..]).chars().map(|c| c.len_utf16()).sum() }; 64 | let #char_len = { 65 | #char_len as #char_len_type 66 | }; 67 | #write_len 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "web")] 2 | pub use ::wasm_bindgen; 3 | 4 | pub use sledgehammer_bindgen_macro::bindgen; 5 | --------------------------------------------------------------------------------