Skip to content

Implement FinalizationRegistry and WeakRef #2593

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

Closed
wants to merge 14 commits into from
Closed
112 changes: 112 additions & 0 deletions std/assembly/finalizationregistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// @ts-ignore: decorators
@lazy let PREVIOUS_FINALIZER: i32 = 0;

// @ts-ignore: decorators
@lazy let INITIALIZED: boolean = false;

// @ts-ignore: decorators
@lazy const ENTRIES_FOR_PTR: Map<usize, FinalizationEntry> = new Map<usize, FinalizationEntry>();

// @ts-ignore: decorators
@lazy let ENTRY_POOL: FinalizationEntry | null = null;

class FinalizationEntry {
next: FinalizationEntry | null = null;
registry: BaseRegistry | null = null;
token: usize = 0;

// @ts-ignore: decorators
@inline
static alloc(): FinalizationEntry {
const instance: FinalizationEntry | null = ENTRY_POOL;
if (instance === null) {
return new FinalizationEntry;
} else {
ENTRY_POOL = instance.next;
return instance;
}
}

// @ts-ignore: decorators
@inline
static recycle(entry: FinalizationEntry): void {
entry.registry = null; // Allow the registry to be GC-ed
entry.next = ENTRY_POOL;
ENTRY_POOL = entry;
}
}

abstract class BaseRegistry {
static finalizeAll(ptr: usize): void {
if (!ENTRIES_FOR_PTR.has(ptr)) { return; }

const entries = ENTRIES_FOR_PTR.get(ptr);

for (
let i: FinalizationEntry | null = entries;
i !== null;
) {
const registry = i.registry;
assert(registry !== null);

registry!.finalize(i.token);
const next: FinalizationEntry | null = i.next;
FinalizationEntry.recycle(i);
i = next;
}

ENTRIES_FOR_PTR.delete(ptr);

if (PREVIOUS_FINALIZER) {
call_indirect<void>(PREVIOUS_FINALIZER, ptr);
}
}

protected abstract finalize(token: usize): void;
}

export class FinalizationRegistry<T> extends BaseRegistry {
private entries: Map<usize, T> = new Map<usize, T>();

constructor(private finalizer: (heldValue: T) => void) {
super();

if (!INITIALIZED) {
PREVIOUS_FINALIZER = __finalize;
__finalize = BaseRegistry.finalizeAll.index;
INITIALIZED = true;
}
}

register<U, V>(target: U, heldValue: T, token: V): void {
if(!isManaged<U>()) { ERROR("invalid target"); }

const targetPtr = changetype<usize>(target);
const tokenPtr = changetype<usize>(token);

if (this.entries.has(tokenPtr)) { return; }
this.entries.set(tokenPtr, heldValue);

const newEntry = FinalizationEntry.alloc();
newEntry.registry = this;
newEntry.token = tokenPtr;
const head: FinalizationEntry | null = ENTRIES_FOR_PTR.has(targetPtr)
? ENTRIES_FOR_PTR.get(targetPtr)
: null;
newEntry.next = head;
ENTRIES_FOR_PTR.set(targetPtr, newEntry);
}

unregister<V>(token: V): bool {
return this.entries.delete(changetype<usize>(token));
}

protected finalize(token: usize): void {
if (this.entries.has(token)) {
const value = this.entries.get(token);
this.entries.delete(token);

this.finalizer(value);
}
}
}
11 changes: 11 additions & 0 deletions std/assembly/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2053,6 +2053,17 @@ declare class Set<K> {
toString(): string;
}

declare class WeakRef<T> {
constructor(value: T);
deref(): T | null;
}

declare class FinalizationRegistry<T> {
constructor(finalizer: (heldValue: T) => void);
register<U, V>(target: U, heldValue: T, token: V): void;
unregister<V>(token: V): bool;
}

interface SymbolConstructor {
readonly hasInstance: symbol;
readonly isConcatSpreadable: symbol;
Expand Down
2 changes: 2 additions & 0 deletions std/assembly/rt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ function __tostack(ptr: usize): usize { // eslint-disable-line
return ptr;
}

export let __finalize: u32 = 0;

// These are provided by the respective implementation, included as another entry file by asc:

// // @ts-ignore: decorator
Expand Down
2 changes: 1 addition & 1 deletion std/assembly/rt/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ declare function __newBuffer(size: usize, id: u32, data?: usize): usize;
declare function __newArray(length: i32, alignLog2: usize, id: u32, data?: usize): usize;

// Finalization
declare function __finalize(ptr: usize): void;
declare let __finalize: i32;

// Debugging
declare const ASC_RTRACE: bool;
Expand Down
4 changes: 2 additions & 2 deletions std/assembly/rt/itcms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,8 @@ function free(obj: Object): void {
obj.prev = changetype<Object>(0);
} else {
total -= obj.size;
if (isDefined(__finalize)) {
__finalize(changetype<usize>(obj) + TOTAL_OVERHEAD);
if (__finalize) {
call_indirect<void>(__finalize, changetype<usize>(obj) + TOTAL_OVERHEAD);
}
__free(changetype<usize>(obj) + BLOCK_OVERHEAD);
}
Expand Down
2 changes: 1 addition & 1 deletion std/assembly/rt/tcms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ export function __collect(): void {
iter.prev = changetype<Object>(0);
} else {
total -= iter.size;
if (isDefined(__finalize)) __finalize(changetype<usize>(iter) + TOTAL_OVERHEAD);
if (__finalize) call_indirect<void>(__finalize, changetype<usize>(iter) + TOTAL_OVERHEAD);
__free(changetype<usize>(iter) + BLOCK_OVERHEAD);
}
iter = newNext;
Expand Down
93 changes: 93 additions & 0 deletions std/assembly/weakmap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// @ts-ignore: decorators
@lazy let TOKEN_POOL: Token | null = null;

// @ts-ignore: decorators
@lazy const REGISTRY: FinalizationRegistry<Token> = new FinalizationRegistry<Token>(
(token: Token) => {
const map = token.map;
if (map) {
map.evict(token.ptr);
}
Token.recycle(token);
}
);

class Token {
map: BaseWeakMap | null = null;
ptr: usize = 0;
next: Token | null = null;

// @ts-ignore: decorators
@inline
static alloc(): Token {
const instance = TOKEN_POOL;
if (instance === null) {
return new Token;
} else {
TOKEN_POOL = instance.next;
return instance;
}
}

// @ts-ignore: decorators
@inline
static recycle(token: Token): void {
token.map = null;
token.next = TOKEN_POOL;
TOKEN_POOL = token;
}
}

abstract class BaseWeakMap {
abstract evict(ptr: usize): void;
}

export class WeakMap<K, V> extends BaseWeakMap {
private data: Map<usize, V> = new Map<usize, V>();
private tokens: Map<usize, Token> = new Map<usize, Token>();

constructor() {
super();

if(!isManaged<K>()) { ERROR("invalid key type"); }
}

get(key: K): V {
return this.data.get(changetype<usize>(key));
}

set(key: K, value: V): void {
const ptr = changetype<usize>(key);
this.data.set(ptr, value);

if (!this.tokens.has(ptr)) {
const token = Token.alloc();
token.map = this;
token.ptr = ptr;

this.tokens.set(ptr, token);
REGISTRY.register(key, token, token);
}
}

has(key: K): bool {
return this.data.has(changetype<usize>(key));
}

delete(key: K): bool {
const ptr = changetype<usize>(key);
if (this.data.delete(ptr)) {
const token = this.tokens.get(ptr);
REGISTRY.unregister(token);
this.tokens.delete(ptr);
return true;
} else {
return false;
}
}

evict(ptr: usize): void {
this.data.delete(ptr);
this.tokens.delete(ptr);
}
}
46 changes: 46 additions & 0 deletions std/assembly/weakref.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// @ts-ignore: decorators
@lazy const COOKIE_FOR_REFERENCE: Map<usize, u32> = new Map<usize, u32>();

// @ts-ignore: decorators
@lazy let CURRENT_COOKIE: u32 = 0;

// @ts-ignore: decorators
@lazy const REGISTRY: FinalizationRegistry<usize> = new FinalizationRegistry<usize>(
(ptr: usize) => {
if (COOKIE_FOR_REFERENCE.delete(ptr)) {
// The memory block could be reused by the allocator after this point
++CURRENT_COOKIE;
}
}
);

export class WeakRef<T> {
private ref: usize;
private cookie: u32;

constructor(value: T) {
if(!isManaged<T>()) { ERROR("invalid target"); }

this.ref = changetype<usize>(value);

if (COOKIE_FOR_REFERENCE.has(this.ref)) {
this.cookie = COOKIE_FOR_REFERENCE.get(this.ref);
} else {
this.cookie = CURRENT_COOKIE;
COOKIE_FOR_REFERENCE.set(this.ref, this.cookie);
REGISTRY.register(value, this.ref, value);
}
}

deref(): T | null {
const ref = this.ref;
if (
COOKIE_FOR_REFERENCE.has(ref)
&& COOKIE_FOR_REFERENCE.get(ref) === this.cookie
) {
return changetype<T>(ref);
} else {
return null;
}
}
}
11 changes: 9 additions & 2 deletions tests/compiler/bindings/noExportRuntime.debug.wat
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
(global $~lib/rt/itcms/iter (mut i32) (i32.const 0))
(global $~lib/rt/itcms/toSpace (mut i32) (i32.const 0))
(global $~lib/rt/itcms/white (mut i32) (i32.const 0))
(global $~lib/rt/__finalize (mut i32) (i32.const 0))
(global $~lib/rt/itcms/fromSpace (mut i32) (i32.const 0))
(global $~lib/rt/tlsf/ROOT (mut i32) (i32.const 0))
(global $~lib/native/ASC_LOW_MEMORY_LIMIT i32 (i32.const 0))
Expand Down Expand Up @@ -1433,8 +1434,14 @@
call $~lib/rt/itcms/Object#get:size
i32.sub
global.set $~lib/rt/itcms/total
i32.const 0
drop
global.get $~lib/rt/__finalize
if
local.get $obj
i32.const 20
i32.add
global.get $~lib/rt/__finalize
call_indirect $0 (type $i32_=>_none)
end
local.get $obj
i32.const 4
i32.add
Expand Down
Loading