locka99 / opcua

A client and server implementation of the OPC UA specification written in Rust
Mozilla Public License 2.0
497 stars 131 forks source link

Compile to WebAssembly / wasm #42

Closed zemirco closed 3 years ago

zemirco commented 4 years ago

Hey,

thank you for creating this library. :100:

I'm trying to use it inside the browser via WebAssembly. The basic idea is to have a WebSocket connection to a server, use the binary protocol for communication and handle serialization/deserialization of messages inside wasm. Within my JavaScript application I can use the wrapper which was automatically created by wasm-pack.

Here is a short demo with the code from one of the tests. It simply converts some bytes into a UAString.

extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn convert(v: &[u8]) -> String {
    let mut stream = Cursor::new(v);
    let decoding_limits = DecodingLimits::default();
    return UAString::decode(&mut stream, &decoding_limits).unwrap().value.unwrap();
}

Compile this code (for Node.js so we don't have to set up a server and start the browser).

wasm-pack build --target nodejs

Call our convert function.

const opcua = require('./pkg/opcua_types')

console.log(opcua.convert([0x06, 0x00, 0x00, 0x00, 0xE6, 0xB0, 0xB4, 0x42, 0x6F, 0x79]))

This returns 水Boy as expected. That's already great :+1:

Now I'm trying to use LocalizedText and I've got several problems. I'm pretty good at JS but not so good with Rust. Maybe you've got an idea.

I tried to add #[wasm_bindgen] to LocalizedText. This gives the error message

the trait string::wasm_bindgen::convert::IntoWasmAbi is not implemented for string::UAString

So I also added #[wasm_bindgen] to UAString. This gives the error message

the trait std::marker::Copy is not implemented for string::UAString

So I tried to add Copy to UAString.

#[derive(Eq, PartialEq, Debug, Clone, Hash, Serialize, Deserialize, Copy)]
pub struct UAString {
    pub value: Option<String>,
}

And finally the compiles tells me that

this field does not implement Copy

So I tried to find a solution for this and found What Is Ownership?. To me it looks like strings do not have a copy method and now I'm stuck.

Any hint you could give me? Thank you very much!

locka99 commented 4 years ago

Hi Mirco, you won't be able to compile OPC UA to web assembly. It has too many external dependencies including on OpenSSL and uses features like multithreading, sockets etc.

If you want to run OPC UA to a web browser then you need to do something like my samples/web-client. This is a webserver with a websocket which is also a compiled OPC UA client that can connect to an OPC UA server. So the logic of OPC is in the webserver itself. The webclient is a HTML, Javascript that communicates to the server with a websocket.

I suspect it might be possible with some effort to make the opcua-types crate to web assembly so that in theory the serializable types could be sent over a websocket but I haven't attempted to try it.

zemirco commented 4 years ago

Hey,

thank you for your answer. I wasn't clear enough, sorry about that. I read through my question and you're right, it was hard to understand what I really want.

I'd like to use ONLY the types, no SSL, no multithreading and no sockets. I know that it's not doable in wasm. I'd like to initiate the WebSocket connection in the JS world. So JS is responsible for creating the socket and also handling security.

I just want to create messages and receive messages. I somehow have to serialize and deserialize the binary packets coming in / going out over the wire. OPC UA has so many types and I don't want to create all of them in JavaScript / TypeScript.

I wrote a huge part of the Go OPC UA implementation https://github.com/gopcua/opcua and did the same. I compiled it to wasm and used it to unpack binary messages. However the wasm is quite large since the whole Go runtime is included. wasm is a first class citizen for Rust and the wasm files are much smaller. That's why I'm asking.

As I said I'm currently stuck at the UAString. It has a string inside and therefore cannot simply implement the copy trait.

Any ideas?

locka99 commented 4 years ago

Okay I understand now. I'm typing this on a mobile so apologies for typos. In Rust you can only implement Copy trait on intrinsic types or things composed from intrinsic types. Anything that involves memory allocation has to implement the Clone trait and an explicit .clone() function. Since a String is allocated that includes that type. I'll see if I can find how you might bind to it from wasm

locka99 commented 4 years ago

I'm learning wasm-bindgen as I go but from the looks of it UAString would have to implement the wasm_bindgen::convert::IntoWasmAbi to know how to marshal the type into JavaScript. It seems odd that it can't do this automatically from that tag although it may be possible to implement the trait manually. I can't say for sure if it would be easy

locka99 commented 4 years ago

I'm still learning about wasm and JS but the issue appears to be that JS and wasm do not share their address / object space so tools like wasm-bindgen do their best to create wrappers that marshal types back and forth for either side to see.

https://rustwasm.github.io/book/reference/js-ffi.html

So this problem with strings seems to be inherent to wasm / js, that because the string is owned by Rust (wasm), it is not easy to make a copy on the JS side and keep them in sync.

If I were you I would ask the wasm-bindgen project if there is something that can be done to overcome this issue.

In the meantime, the problem boils down marshalling a type across the boundary between JS and wasm. Internal to wasm you should be clear to use the type as-is. So perhaps instead of trying to get the type out you should use JSON on the JS side and your boundary function should deserialize the JSON string into the appropriate struct using serde.

At present I only implement serde's Serialize / Deserialize traits on Variant and types that a Variant can contain, but it might prove the concept.

If you look for json_serde::from_str and json_serde::to_string you should see how a type can be turned to and from a string. I use these calls in some unit tests and in the web-client sample.

zemirco commented 4 years ago

Thank you for digging into this. That's a great feedback although we didn't really find a solution. However I will start looking into your ideas.

locka99 commented 4 years ago

I've opened a question on wasm-bindgen to see if they have any general help on the Option thing because it's not specific to my code but seems useful.

In the meantime I've also moved some code out of types into core to cut down the dependencies a little bit.

zemirco commented 4 years ago

Unfortunately the issue was closed without a lot of information. Maybe we can work on this together. I've taken your example and tried to add the setters and getters as described in the other issue.

pub struct Test {
   pub value: Option<String>
}

#[wasm_bindgen]
impl Test {

    #[wasm_bindgen(getter)]
    pub fn value(&self) -> String {
        if self.value.is_some() {
            return self.value.unwrap()
        }
        return "".to_string()
    }

    #[wasm_bindgen(setter)]
    pub fn set_field(&mut self, value: String) {
        self.value = Some(value);
        // self.value = value;
    }

}

Now I'm getting different errors.

error[E0277]: the trait bound `string::Test: string::wasm_bindgen::convert::RefFromWasmAbi` is not satisfied
  --> types/src/string.rs:32:1
   |
32 | #[wasm_bindgen]
   | ^^^^^^^^^^^^^^^ the trait `string::wasm_bindgen::convert::RefFromWasmAbi` is not implemented for `string::Test`

error[E0277]: the trait bound `string::Test: string::wasm_bindgen::convert::RefMutFromWasmAbi` is not satisfied
  --> types/src/string.rs:32:1
   |
32 | #[wasm_bindgen]
   | ^^^^^^^^^^^^^^^ the trait `string::wasm_bindgen::convert::RefMutFromWasmAbi` is not implemented for `string::Test`

error: aborting due to 2 previous errors

Just wanted to let you know what I'm currently trying. I've got meetings now but will hopefully work on this later.

locka99 commented 4 years ago

This seems to work:

#[wasm_bindgen]
pub struct TestString {
    value: Option<String>
}

#[wasm_bindgen]
impl TestString {
    pub fn new(value: &str) -> TestString {
        TestString { value: Some(value.into()) }
    }

    pub fn value(&self) -> Option<String> {
        self.value.clone()
    }

    pub fn set_value(&mut self, value: Option<String>) {
        self.value = value;
    }
}

So value is not public any more and there are setter/getters on the impl to get the value (via a clone()) or set it.

By "work" I mean it generates pkg/wasm_types.js with a TestString struct in it which I could presumably say from JS - console.log(TestString.new("Hello World").value()) and get an object of that type. I haven't tested it. It's worth looking at what it does generate though because it looks like it basically generates wrappers around Rust types to marshal things in and out of JS, which is why there are all these rules and conditions.

I have modified the real UAString to make the internal value non-public however my getter currently returns a reference, which wasm-pack doesn't like - it doesn't like borrowed values being returned from functions.

For the rest of the structs in opcua-types you may be compelled to do something similar. Some structs contain Vec<> fields so they might need a similar getter and setter. I'd suggest forking opcua and maybe playing with `tools/schema/gen_types.js" because that's the script I use to machine generate most of the files. Maybe you could modify it to add getters and setters for all the fields and make the fields themselves private.

locka99 commented 3 years ago

Closing since it's unlikely to compile to wasm any time soon