Skip to content

Commit

Permalink
feat(dan): template macro handles component state (#4380)
Browse files Browse the repository at this point in the history
Description
---
* Function parameters are correctly encoded and passed to the user function
* Component parameters (i.e. `self`) are transparently handled to the user code:
    1. The ABI specifies a `u32` type component id
    2. The state is retrieved from the engine with that component id (not the real call yet, as it's not implemented in the engine)
    3. The state is decoded and passed to the function on the `self` parameter
    4. If the attribute is mutable (`&mut self`) then we call the engine to update the component state
* Unit return types (`()`) are now supported in functions (see the `State.set` function as an example)
* Expanded the `ast.rs` module with convenient functions to detect `self` and constructor functions

Motivation and Context
---
Following the previous work on the template macro (#4358, #4361), this PR aims to solve some of the previous limitations:
* The function arguments must be processed and encoded
* Struct fields and `self` must be handled
* Calls to the tari engine import should be done instead of mocked

With those implemented, the `state` test example is now written as:
```
use tari_template_macros::template;

#[template]
mod state_template {
    pub struct State {
        pub value: u32,
    }

    impl State {
        pub fn new() -> Self {
            Self { value: 0 }
        }

        pub fn set(&mut self, value: u32) {
            self.value = value;
        }

        pub fn get(&self) -> u32 {
            self.value
        }
    }
}
```
Please keep in mind that this is the simplest example that manages the contract state, but it currently supports function logic as complex as the user wants, as long as it is valid Rust code.

Also, for now I didn't find necessary to mark constructor functions in any special way. Right now, a function is automatically considered a constructor (and the component instantiated) if it returns `Self`.

Lastly, as the state managing itself is not yet implemented on the engine, state is not conserved between calls. But this PR encapsulates component related logic in a single module (`component.rs`) so it should be relatively simple to implement in the future.

How Has This Been Tested?
---
The unit test for the `state` example, now rewritten using the template macro, pass
  • Loading branch information
mrnaveira authored Aug 3, 2022
1 parent b56c63a commit 696d909
Show file tree
Hide file tree
Showing 15 changed files with 412 additions and 237 deletions.
1 change: 1 addition & 0 deletions dan_layer/engine/tests/hello_world/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 14 additions & 2 deletions dan_layer/engine/tests/state/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions dan_layer/engine/tests/state/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ edition = "2021"
[dependencies]
tari_template_abi = { path = "../../../template_abi" }
tari_template_lib = { path = "../../../template_lib" }
tari_template_macros = { path = "../../../template_macros" }

[profile.release]
opt-level = 's' # Optimize for size.
Expand Down
109 changes: 4 additions & 105 deletions dan_layer/engine/tests/state/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,15 @@
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

use tari_template_abi::{decode, encode_with_len, FunctionDef, Type};
use tari_template_lib::{call_engine, generate_abi, generate_main, TemplateImpl};
use tari_template_macros::template;

// that's what the example should look like from the user's perspective
#[allow(dead_code)]
#[template]
mod state_template {
use tari_template_abi::{borsh, Decode, Encode};

// #[tari::template]
#[derive(Encode, Decode)]
pub struct State {
value: u32,
pub value: u32,
}

// #[tari::impl]
impl State {
// #[tari::constructor]
pub fn new() -> Self {
Self { value: 0 }
}
Expand All @@ -49,98 +41,5 @@ mod state_template {
self.value
}
}
}

// TODO: Macro generated code
#[no_mangle]
extern "C" fn State_abi() -> *mut u8 {
let template_name = "State".to_string();

let functions = vec![
FunctionDef {
name: "new".to_string(),
arguments: vec![],
output: Type::U32, // the component_id
},
FunctionDef {
name: "set".to_string(),
arguments: vec![Type::U32, Type::U32], // the component_id and the new value
output: Type::Unit, // does not return anything
},
FunctionDef {
name: "get".to_string(),
arguments: vec![Type::U32], // the component_id
output: Type::U32, // the stored value
},
];

generate_abi(template_name, functions)
}

#[no_mangle]
extern "C" fn State_main(call_info: *mut u8, call_info_len: usize) -> *mut u8 {
let mut template_impl = TemplateImpl::new();
use tari_template_abi::{ops::*, CreateComponentArg, EmitLogArg, LogLevel};
use tari_template_lib::models::ComponentId;

tari_template_lib::call_engine::<_, ()>(OP_EMIT_LOG, &EmitLogArg {
message: "This is a log message from State_main!".to_string(),
level: LogLevel::Info,
});

// constructor
template_impl.add_function(
"new".to_string(),
Box::new(|_| {
let ret = state_template::State::new();
let encoded = encode_with_len(&ret);
// Call the engine to create a new component
// TODO: proper component id
// The macro will know to generate this call because of the #[tari(constructor)] attribute
// TODO: what happens if the user wants to return multiple components/types?
let component_id = call_engine::<_, ComponentId>(OP_CREATE_COMPONENT, &CreateComponentArg {
name: "State".to_string(),
quantity: 1,
metadata: Default::default(),
state: encoded,
});
let component_id = component_id.expect("no asset id returned");
encode_with_len(&component_id)
}),
);

template_impl.add_function(
"set".to_string(),
Box::new(|args| {
// read the function paramenters
let _component_id: u32 = decode(&args[0]).unwrap();
let _new_value: u32 = decode(&args[1]).unwrap();

// update the component value
// TODO: use a real op code (not "123") when they are implemented
call_engine::<_, ()>(123, &());

// the function does not return any value
// TODO: implement "Unit" type empty responses. Right now this fails: wrap_ptr(vec![])
encode_with_len(&0)
}),
);

template_impl.add_function(
"get".to_string(),
Box::new(|args| {
// read the function paramenters
let _component_id: u32 = decode(&args[0]).unwrap();

// get the component state
// TODO: use a real op code (not "123") when they are implemented
let _state = call_engine::<_, ()>(123, &());

// return the value
let value = 1_u32; // TODO: read from the component state
encode_with_len(&value)
}),
);

generate_main(call_info, call_info_len, template_impl)
}
}
8 changes: 4 additions & 4 deletions dan_layer/engine/tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,25 +46,25 @@ fn test_hello_world() {

#[test]
fn test_state() {
// TODO: use the Component and ComponentId types in the template
let template_test = TemplateTest::new("State".to_string(), "tests/state".to_string());

// constructor
let component: ComponentId = template_test.call_function("new".to_string(), vec![]);
assert_eq!(component.1, 0);
let component: ComponentId = template_test.call_function("new".to_string(), vec![]);
assert_eq!(component.1, 1);

// call the "set" method to update the instance value
let new_value = 20_u32;
template_test.call_method::<()>("State".to_string(), "set".to_string(), vec![
encode_with_len(&component),
encode_with_len(&new_value),
]);

// call the "get" method to get the current value
let value: u32 = template_test.call_method("State".to_string(), "get".to_string(), vec![encode_with_len(
&component,
)]);
assert_eq!(value, 1);
// TODO: when state storage is implemented in the engine, assert the previous setted value (20_u32)
assert_eq!(value, 0);
}

struct TemplateTest {
Expand Down
20 changes: 1 addition & 19 deletions dan_layer/template_lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub mod models;

// TODO: we should only use stdlib if the template dev needs to include it e.g. use core::mem when stdlib is not
// available
use std::{collections::HashMap, mem, ptr::copy, slice};
use std::{collections::HashMap, mem, slice};

use tari_template_abi::{encode_with_len, Decode, Encode, FunctionDef, TemplateDef};

Expand Down Expand Up @@ -119,21 +119,3 @@ pub fn call_debug<T: AsRef<[u8]>>(data: T) {
unsafe { debug(ptr, len) }
}

#[no_mangle]
pub unsafe extern "C" fn tari_alloc(len: u32) -> *mut u8 {
let cap = (len + 4) as usize;
let mut buf = Vec::<u8>::with_capacity(cap);
let ptr = buf.as_mut_ptr();
mem::forget(buf);
copy(len.to_le_bytes().as_ptr(), ptr, 4);
ptr
}

#[no_mangle]
pub unsafe extern "C" fn tari_free(ptr: *mut u8) {
let mut len = [0u8; 4];
copy(ptr, len.as_mut_ptr(), 4);

let cap = (u32::from_le_bytes(len) + 4) as usize;
let _ = Vec::<u8>::from_raw_parts(ptr, cap, cap);
}
38 changes: 38 additions & 0 deletions dan_layer/template_lib/src/models/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,42 @@
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

// TODO: use the actual component id type
pub type ComponentId = ([u8; 32], u32);

use tari_template_abi::{Decode, Encode, encode_with_len, ops::OP_CREATE_COMPONENT, CreateComponentArg};

use crate::call_engine;

pub fn initialise<T: Encode + Decode>(template_name: String, initial_state: T) -> ComponentId {
let encoded_state = encode_with_len(&initial_state);

// Call the engine to create a new component
// TODO: proper component id
// TODO: what happens if the user wants to return multiple components/types?
let component_id = call_engine::<_, ComponentId>(OP_CREATE_COMPONENT, &CreateComponentArg {
name: template_name,
quantity: 1,
metadata: Default::default(),
state: encoded_state,
});
component_id.expect("no asset id returned")
}

pub fn get_state<T: Encode + Decode>(_id: u32) -> T {
// get the component state
// TODO: use a real op code (not "123") when they are implemented
let _state = call_engine::<_, ()>(123, &());

// create and return a mock state because state is not implemented yet in the engine
let len = std::mem::size_of::<T>();
let byte_vec = vec![0_u8; len];
let mut mock_value = byte_vec.as_slice();
T::deserialize(&mut mock_value).unwrap()
}

pub fn set_state<T: Encode + Decode>(_id: u32, _state: T) {
// update the component value
// TODO: use a real op code (not "123") when they are implemented
call_engine::<_, ()>(123, &());
}
2 changes: 1 addition & 1 deletion dan_layer/template_lib/src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

mod component;
pub use component::ComponentId;
pub use component::*;
8 changes: 8 additions & 0 deletions dan_layer/template_macros/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions dan_layer/template_macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ proc-macro = true

[dependencies]
tari_template_abi = { path = "../template_abi" }
tari_template_lib = { path = "../template_lib" }
syn = { version = "1.0.98", features = ["full"] }
proc-macro2 = "1.0.42"
quote = "1.0.20"
Expand Down
Loading

0 comments on commit 696d909

Please sign in to comment.