Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send safe roc list #4361

Merged
merged 4 commits into from
Oct 21, 2022
Merged

Send safe roc list #4361

merged 4 commits into from
Oct 21, 2022

Conversation

bhansconnect
Copy link
Contributor

This was a request that came up when using Roc types with threads and async. They get quite inconvenient to pass from one thread to the next. These Wrappers should make that simpler.

Copy link
Member

@BrianHicks BrianHicks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm excited for this! Thank you for doing it! I have some questions, though.

}

// Marks a list as readonly. This means that it will be leaked.
// For constants passed in from platform to application, this may be reasonable.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't this the other way around? It's a bit of memory passed from the application to the platform?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, so this has a very specific use case in mind. I have a Rust host. It spawns a task/future/thread per request. There is a constant Str or List that I want to pass into my Roc function on every request. I do not want to allocate and deallocate on every single one of these calls. That would just be a waste.

Instead, I create a RocStr. I set it as readonly. I then share it between as many threads as I want, cloning the pointer, capacity, and length, rather than cloning the entire pointed to Str and calling malloc.

I specifically ran into this with a web server. Every request, I want to pass in the base url of my website. As a result, there was an extra malloc and free for every request that I would rather avoid if possible.


If something is passed from application to host, I would generally not expect that this function would get used, but I guess you might have uses.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, interesting! That makes a lot of sense.


// Marks a list as readonly. This means that it will be leaked.
// For constants passed in from platform to application, this may be reasonable.
// Marked unsafe because it should not be used lightly.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't "unsafe" in Rust mean memory unsafety? How could this lead to that? If anything, it's making things a little safer by avoiding in-place mutation for the rest of this list's life?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is memory unsafe, at least in my opinion. It leads to a memory leak, 100% guaranteed. If used wrong, this could lead to a crash due to eating up all memory.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wait, how does it lead to a leak? Because it needs to be copied to be used? It's still being tracked, isn't it? It's just up to the platform to manage the lifecycle after this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok so it technically doesn't have to lead to a leak, but it would by default.

In general, read-only can appear in 3 ways:

  1. The value is stored in read-only memory like a constant in the app
  2. The value is set read-only by the host.
  3. Our refcounting maxes out. When that happens, we saturate to read-only and the value is never freed.

In the host case. I would expect most users to set a value to read-only and then keep it around for the lifetime of the app, repeatedly passing the value in. If instead, you set a value to read-only and then drop it, you will get a memory leak. A read only value is never freed (we don't know how it became read-only and are not sure if it is safe to free. might lead to use after free).

Technically a smart platform that knows lifetimes could use it as needed and then set it back to regularly refcounted and free the value. That said, if any roc code still has a reference to it, that would lead to use after free. As such, if you set a value to read-only, you really are labeling it as having a static lifetime that lasts until the end of a program.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahhhh, ok. That makes a lot of sense. Maybe this explanation belongs in the doc comment? I think clippy will warn if there isn't a section starting with "unsafety:" anyway, right? (We just don't enforce clippy-cleanliness on roc_std)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a safety comment. Also filed #4366 about adding clippy and tests to CI for these crates.

T: Clone,
{
fn from(l: RocList<T>) -> Self {
if l.is_unique() || l.is_readonly() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this also need to check if each item is unique/readonly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, but maybe a better api could be made. Currently, a RocList will only be Send if every element it contains is Send. That comes from the where T: Send a few lines up.

As such, If you make a SendSafeRocList<RocStr>, it will not be Send. You would need to first run a map over the list to create a SendSafeRocList<SendSafeRocStr> and then it would be sendable. So this only needs to make the outer list Send. Rust will enforce that the inner item is also Send before allow it to be sent.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that seems reasonable for now. It seems like an OK to manage this automatically would be to define some ConvertToSendable trait. I don't know if that's what we want, though!

crates/roc_std/src/roc_list.rs Show resolved Hide resolved
@bhansconnect
Copy link
Contributor Author

General question: Do you think I should add Clone to the SendSafe* types?

That way, you can Clone them without unwrapping and rewrapping them. In the case the item is readonly, it would do a shallow copy. In the case that the item is just unique, it would do a deep copy.

This would be used when you want to send the same data to many threads. So you would clone it for each thread and pass the clone into the thread.

@BrianHicks
Copy link
Member

shallow copy meaning copying the pointer/len/capacity? I think that'd probably make sense, but I don't have a use case right now where I'd need it. Maybe you do with your web server?

BrianHicks
BrianHicks previously approved these changes Oct 19, 2022
@bhansconnect bhansconnect merged commit 9f8bf3f into main Oct 21, 2022
@bhansconnect bhansconnect deleted the send-safe-roc-list branch October 21, 2022 08:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants