-
-
Notifications
You must be signed in to change notification settings - Fork 670
Suggestion: simple type-checked pointer primitive #1363
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
Comments
btw if you want really zero-cost pointer abstraction it better use something like this: @final class ptr<T> {
@inline constructor(offset: usize = 0) {
return changetype<ptr<T>>(offset);
}
@inline get deref(): T {
return changetype<T>(this);
}
@inline set deref(value: T) {
store<T>(changetype<usize>(this), value);
}
}
let fooPtr = new ptr<i32>(0x200);
let foo = fooPtr.deref; |
The one above is already zero-cost and doesn't require You could simulate the same via inlined getters/setters, but IMO it's pointless and further from zero-cost, when you can just use a field :) |
It zero-cost only if you store / load from |
I didn't suggest creating pointers - for constructor I was thinking of either making it private (to make it truly opaque and leave up to people to use That's probably worth a separate discussion, my main focus is the |
Btw, your get/set code above doesn't compile in couple of places: @final class ptr<T> {
@inline constructor(offset: usize = 0) {
return changetype<ptr<T>>(offset);
}
@inline get deref(): T {
return changetype<T>(this); // Type 'main/ptr<f32>' cannot be changed to type 'f32'.
}
@inline set deref(value: T): void { // A 'set' accessor cannot have a return type annotation.
store<T>(changetype<usize>(this), value);
}
} It's not hard to fix up like this: @final class ptr<T> {
@inline constructor(offset: usize = 0) {
return changetype<ptr<T>>(offset);
}
@inline get deref(): T {
return load<T>(changetype<usize>(this));
}
@inline set deref(value: T) {
store<T>(changetype<usize>(this), value);
}
} but just shows yet another reason to avoid bypassing typechecker and keeping code simple. After all, static types is the strong suit of TypeScript / AssemblyScript - best to use them directly as much possible, without |
Fixed. This a fiddle: https://webassembly.studio/?f=b1m6z7wfv5h |
Yeah I was fixing up in fiddle too. |
But, again, compare the complexity of those conversions and just What you're designing looks more like the
and precisely what I wanted to avoid by suggesting a lower-level primitive. |
But without that changes Anyway this suggestion makes sense but in my opinion it should be built-in abstraction type for keeping efficiency and more powerful semantic checking. cc @dcodeIO |
Only if you create it. If the goal is to just receive them via FFI, it doesn't matter. But if you want to return pointers from a function to JS so that it could store and return them later like export function foo(): ptr<f32> {
return new ptr<f32>(1.2);
} then the default heap-based constructor makes sense IMO, since any other pointer wouldn't survive. So yeah, constructor is |
Perhaps both concepts can be merged into one, so we get the simplicity of the |
I kinda prefer
I think pointer class can extend the low-level primitive, adding more methods, but for those who don't opt-in, they can use
|
Also if class Foo {
method(): i32 { ... }
}
let fooPtr = new ptr<Foo>();
fooPtr.method(); // which check holded raw pointer value with null and only after that make `method` call which actually will be: let foo = fooPtr.deref;
if (foo !== null) foo.method();
else throw new Error('null pointer deref'); And it could be even statically check if this |
However perhaps better just make let fooPtr = new ptr<Foo>(...);
fooPtr.deref?.method();
// or
fooPtr.deref!.method(); |
|
Hmm, may be I'm not understand main goals of proposal. I was under impression that
Yes, but in Rust, C++ and AS at least first 4 bytes reserved for null pointer. However you could write to this memory position without any restriction, that's true I try to build analogies with C pointers:
Is it make sense? |
Yeah, but such wrapping is not necessary for classes, because they are already represented with pointers as-is. It's mostly primitives that are a problem (e.g. Well, it will also help with some cases where target expects a |
Not really; Just like |
@RReverser except in Web Assembly the stack is virtual. You can't point to something on the stack. Edit: In fact! When rust compiles to web assembly, if it needs to become a ptr, I bet it becomes effectively boxed. |
I believe I've surfaced complaints about the ergonomics of |
Let's look at another language with managed types, whose boxing was long before Rust. I mean C#. There are has ability to "call-by-value" and "call-by-reference" put "out" or "ref" modifiers before formal and actual parameters like: void CallByRef(ref int num) {
num = 111;
}
int res = 0;
CallByRef(ref res);
// res has 111 value now The same thing could be simulate via boxing / unboxing (to And on Rust variants: pub fn call_by_ref(num: &mut i32) {
*num = 111;
}
pub fn call_by_ref_via_box(mut num: Box<i32>) {
*num = 111;
} But on Rust it's works differently under the hood due to Rust has shadow stack and don't need implicitly boxing for |
@MaxGraey TBH, I don't know where this discussion is going / what is the purpose, it seems to have become a bit too long and goes into language theory discussions :) Let's focus on practical side: are you trying to figure out how this wrapper is represented or how it's going to be used or ..? I want to understand how I can improve my explanations above in case they weren't clear. |
No, this is not true. In native languages / machines stack and heap also technically live in the same memory, but language-level semantics of All in all, I feel that |
I'm just trying to understand all aspects and find a more generalized solution. Because this proposal looks too specific to support at the language or runtime level. |
Looks like the confusion here comes from Regarding Box: Such a pointer and a |
Regarding making this a complete concept: Perhaps we should go an rename
Unfortunately this conflicts with just a |
It could lead to wrong assumption about that class is allocated on stack and passed by value which is not true |
The renaming is not ultimately necessary if you think that'd be confusing. Making this a complete concept would be more important when we want to add it to stdlib. |
@dcodeIO I like your suggestion, although I'm not sure special handling for structs/classes is worth the complexity. To me, it feels a bit magical and I'd rather teach users that objects are already pointers on their own. I'm not strongly opposed to the special handling, just not sure whether it's worth it. |
This is a good summary too, by the way. |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
That seems like a short level for stale issues 😅 |
Whoops, forgot to add a label :) |
In many cases, I find it useful to have type-checked pointers (as opposed to just
usize
) to make sure that values loaded / stored in memory are indeed correct for the given pointer across the FFI boundary.I've seen #1228, and, in particular the Pointer experiment reference there (https://github.com/AssemblyScript/assemblyscript/blob/master/tests/compiler/std/pointer.ts#L3) and I think it's a nice high-level API, but perhaps unnecessarily high-level for majority of cases.
On the other hand, the current primitives (
load
/store
) are too low-level - they don't provide any typechecking whatsoever, and allow storing arbitrary data to arbitrary pointers.I propose a solution somewhere in between these two that I've come to use myself - a primitive for opaque pointers:
This doesn't provide nice high-level APIs for pointer arithmetic, but in many cases it's not necessary. Often enough, as a user, you simply want to accept an opaque typechecked pointer (which is represented as
usize
under the hood), and read or write data to it, and be done with it. This is what such class provides.class
is already represented by AssemblyScript as a pointer to the actual data, and in this case data is whatever theT
is, so you can read and write data via such helper by simply:Compare this to a "raw" solution:
Both compile to precisely same code (in fact, they get merged by Binaryen if put in the same file), but in the latter case it's too easy for different invocations on the same pointer to accidentally get out of sync in terms of types, as well as public API is less clear on what
f
is supposed to contain.Anyway, just thought I'd post this and would love to hear your thoughts.
The text was updated successfully, but these errors were encountered: