@pistonite/pure 0.25.0 → 0.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/memory/erc.test.ts +267 -0
- package/src/memory/erc.ts +316 -0
- package/src/memory/index.ts +1 -1
- package/src/result/index.ts +27 -8
- package/src/memory/weak.ts +0 -140
package/package.json
CHANGED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { describe, afterEach, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { type Erc, makeErcType } from "./erc.ts";
|
|
4
|
+
|
|
5
|
+
type Rc = {
|
|
6
|
+
value: string;
|
|
7
|
+
refCount: number;
|
|
8
|
+
};
|
|
9
|
+
const Marker = Symbol("test");
|
|
10
|
+
type Marker = typeof Marker;
|
|
11
|
+
class Allocator {
|
|
12
|
+
private mockMemory: (Rc | undefined)[] = [];
|
|
13
|
+
public makeTestErc: (ptr: number) => Erc<Marker>;
|
|
14
|
+
|
|
15
|
+
// double indirection means each Erc holds a pointer to the external smart pointer,
|
|
16
|
+
// which also means external smart pointers all need to be heap allocated
|
|
17
|
+
// addRef() will return a pointer to a new external smart pointer
|
|
18
|
+
//
|
|
19
|
+
// single indirection on the other hand, means each Erc holds a pointer
|
|
20
|
+
// to the object directly. and addRef() will return the same pointer
|
|
21
|
+
//
|
|
22
|
+
// The Erc implementation must work with both
|
|
23
|
+
constructor(isDoubleIndirection: boolean) {
|
|
24
|
+
this.makeTestErc = makeErcType({
|
|
25
|
+
marker: Marker,
|
|
26
|
+
free: (ptr: number) => {
|
|
27
|
+
if (ptr < 0 || ptr >= this.mockMemory.length) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Invalid index into mock memory. Length is ${this.mockMemory.length} but index is ${ptr}`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const rc = this.mockMemory[ptr];
|
|
34
|
+
if (!rc) {
|
|
35
|
+
throw new Error("Double free detected");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
rc.refCount--;
|
|
39
|
+
if (isDoubleIndirection) {
|
|
40
|
+
this.mockMemory[ptr] = undefined;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (rc.refCount === 0) {
|
|
45
|
+
this.mockMemory[ptr] = undefined;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
addRef: (value: number) => {
|
|
49
|
+
if (value < 0 || value >= this.mockMemory.length) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Invalid index into mock memory. Length is ${this.mockMemory.length} but index is ${value}`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const rc = this.mockMemory[value];
|
|
56
|
+
if (!rc) {
|
|
57
|
+
throw new Error("AddRef on freed memory detected");
|
|
58
|
+
}
|
|
59
|
+
rc.refCount++;
|
|
60
|
+
if (isDoubleIndirection) {
|
|
61
|
+
for (let i = 0; i < this.mockMemory.length; i++) {
|
|
62
|
+
if (this.mockMemory[i] === undefined) {
|
|
63
|
+
this.mockMemory[i] = rc;
|
|
64
|
+
return i;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
this.mockMemory.push(rc);
|
|
68
|
+
return this.mockMemory.length - 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return value;
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
allocValue(value: string): number {
|
|
77
|
+
const rc: Rc = {
|
|
78
|
+
value,
|
|
79
|
+
refCount: 1,
|
|
80
|
+
};
|
|
81
|
+
for (let i = 0; i < this.mockMemory.length; i++) {
|
|
82
|
+
if (this.mockMemory[i] === undefined) {
|
|
83
|
+
this.mockMemory[i] = rc;
|
|
84
|
+
return i;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
this.mockMemory.push(rc);
|
|
88
|
+
return this.mockMemory.length - 1;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
getValue(ptr: number | undefined): string {
|
|
92
|
+
if (ptr === undefined) {
|
|
93
|
+
throw new Error("Dereference of nullptr");
|
|
94
|
+
}
|
|
95
|
+
if (ptr < 0 || ptr >= this.mockMemory.length) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Invalid index into mock memory. Length is ${this.mockMemory.length} but index is ${ptr}`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const rc = this.mockMemory[ptr];
|
|
102
|
+
if (!rc) {
|
|
103
|
+
throw new Error("Dangling pointer");
|
|
104
|
+
}
|
|
105
|
+
return rc.value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
expectNoLeak(): void {
|
|
109
|
+
for (let i = 0; i < this.mockMemory.length; i++) {
|
|
110
|
+
const rc = this.mockMemory[i];
|
|
111
|
+
if (rc) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Memory leak detected at index ${i}. Value: ${rc.value}, RefCount: ${rc.refCount}`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
cleanup(): void {
|
|
120
|
+
this.mockMemory = [];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
describe.each`
|
|
125
|
+
indirection | allocator
|
|
126
|
+
${"single"} | ${new Allocator(false)}
|
|
127
|
+
${"double"} | ${new Allocator(true)}
|
|
128
|
+
`(
|
|
129
|
+
"Erc - $indirection indirection",
|
|
130
|
+
({ allocator }: { allocator: Allocator }) => {
|
|
131
|
+
afterEach(() => {
|
|
132
|
+
allocator.cleanup();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("allocate and deallocate correctly", () => {
|
|
136
|
+
const test = allocator.makeTestErc(allocator.allocValue("Hello"));
|
|
137
|
+
expect(allocator.getValue(test.value)).toBe("Hello");
|
|
138
|
+
test.free();
|
|
139
|
+
allocator.expectNoLeak();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("frees if assigned new value", () => {
|
|
143
|
+
const test = allocator.makeTestErc(allocator.allocValue("Hello"));
|
|
144
|
+
test.assign(allocator.allocValue("World"));
|
|
145
|
+
expect(allocator.getValue(test.value)).toBe("World");
|
|
146
|
+
test.free();
|
|
147
|
+
allocator.expectNoLeak();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("does not free when taking value", () => {
|
|
151
|
+
const test = allocator.makeTestErc(allocator.allocValue("Hello"));
|
|
152
|
+
const raw = test.take();
|
|
153
|
+
expect(allocator.getValue(raw)).toBe("Hello");
|
|
154
|
+
expect(test.value).toBeUndefined();
|
|
155
|
+
if (raw === undefined) {
|
|
156
|
+
throw new Error("Raw value is undefined");
|
|
157
|
+
}
|
|
158
|
+
allocator.makeTestErc(raw).free();
|
|
159
|
+
allocator.expectNoLeak();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("invalidates weak references on free", () => {
|
|
163
|
+
const test = allocator.makeTestErc(allocator.allocValue("Hello"));
|
|
164
|
+
const testWeak = test.getWeak();
|
|
165
|
+
expect(allocator.getValue(testWeak.value)).toBe("Hello");
|
|
166
|
+
test.free();
|
|
167
|
+
expect(testWeak.value).toBeUndefined();
|
|
168
|
+
allocator.expectNoLeak();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("invalidates weak references on assign", () => {
|
|
172
|
+
const test = allocator.makeTestErc(allocator.allocValue("Hello"));
|
|
173
|
+
const testWeak = test.getWeak();
|
|
174
|
+
expect(allocator.getValue(testWeak.value)).toBe("Hello");
|
|
175
|
+
test.assign(allocator.allocValue("World"));
|
|
176
|
+
expect(testWeak.value).toBeUndefined();
|
|
177
|
+
test.free();
|
|
178
|
+
allocator.expectNoLeak();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("handles assign and take of different references correctly", () => {
|
|
182
|
+
const test1 = allocator.makeTestErc(allocator.allocValue("Hello"));
|
|
183
|
+
const test2 = allocator.makeTestErc(allocator.allocValue("World"));
|
|
184
|
+
expect(allocator.getValue(test1.value)).toBe("Hello");
|
|
185
|
+
expect(allocator.getValue(test2.value)).toBe("World");
|
|
186
|
+
test1.assign(test2.take());
|
|
187
|
+
expect(allocator.getValue(test1.value)).toBe("World");
|
|
188
|
+
test1.free();
|
|
189
|
+
allocator.expectNoLeak();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("handles assign and take of same references correctly", () => {
|
|
193
|
+
const test1 = allocator.makeTestErc(allocator.allocValue("Hello"));
|
|
194
|
+
const test2 = test1.getStrong();
|
|
195
|
+
test1.assign(test2.take());
|
|
196
|
+
expect(allocator.getValue(test1.value)).toBe("Hello");
|
|
197
|
+
test1.free();
|
|
198
|
+
test2.free(); // should be no-op
|
|
199
|
+
allocator.expectNoLeak();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("assigning another Erc directly should cause double free", async () => {
|
|
203
|
+
const test1 = allocator.makeTestErc(allocator.allocValue("Hello"));
|
|
204
|
+
const test2 = test1.getStrong();
|
|
205
|
+
test1.assign(test2.value);
|
|
206
|
+
expect(allocator.getValue(test1.value)).toBe("Hello");
|
|
207
|
+
test1.free();
|
|
208
|
+
|
|
209
|
+
const freeTest2 = async () => {
|
|
210
|
+
test2.free();
|
|
211
|
+
};
|
|
212
|
+
await expect(freeTest2).rejects.toThrow("Double free detected");
|
|
213
|
+
allocator.expectNoLeak();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("handles assign and take of same references correctly (same Erc)", () => {
|
|
217
|
+
const test1 = allocator.makeTestErc(allocator.allocValue("Hello"));
|
|
218
|
+
test1.assign(test1.take());
|
|
219
|
+
expect(allocator.getValue(test1.value)).toBe("Hello");
|
|
220
|
+
test1.free();
|
|
221
|
+
allocator.expectNoLeak();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("assigning another Erc directly should cause double free (same Erc)", async () => {
|
|
225
|
+
const test1 = allocator.makeTestErc(allocator.allocValue("Hello"));
|
|
226
|
+
test1.assign(test1.value);
|
|
227
|
+
const getTest1Value = async () => {
|
|
228
|
+
allocator.getValue(test1.value);
|
|
229
|
+
};
|
|
230
|
+
await expect(getTest1Value).rejects.toThrow("Dangling pointer");
|
|
231
|
+
|
|
232
|
+
const freeTest1 = async () => {
|
|
233
|
+
test1.free();
|
|
234
|
+
};
|
|
235
|
+
await expect(freeTest1).rejects.toThrow("Double free detected");
|
|
236
|
+
allocator.expectNoLeak();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("inc ref count with strong reference", () => {
|
|
240
|
+
const test = allocator.makeTestErc(allocator.allocValue("Hello"));
|
|
241
|
+
const test2 = test.getStrong();
|
|
242
|
+
expect(allocator.getValue(test.value)).toBe("Hello");
|
|
243
|
+
expect(allocator.getValue(test2.value)).toBe("Hello");
|
|
244
|
+
test.free();
|
|
245
|
+
expect(allocator.getValue(test2.value)).toBe("Hello");
|
|
246
|
+
test2.free();
|
|
247
|
+
allocator.expectNoLeak();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("inc ref count with strong reference from weak reference", () => {
|
|
251
|
+
const test = allocator.makeTestErc(allocator.allocValue("Hello"));
|
|
252
|
+
const testWeak = test.getWeak();
|
|
253
|
+
expect(allocator.getValue(testWeak.value)).toBe("Hello");
|
|
254
|
+
const test2 = testWeak.getStrong();
|
|
255
|
+
expect(allocator.getValue(testWeak.value)).toBe("Hello");
|
|
256
|
+
expect(allocator.getValue(test2.value)).toBe("Hello");
|
|
257
|
+
const test2Weak = test2.getWeak();
|
|
258
|
+
test.free();
|
|
259
|
+
expect(testWeak.value).toBeUndefined();
|
|
260
|
+
expect(allocator.getValue(test2.value)).toBe("Hello");
|
|
261
|
+
expect(allocator.getValue(test2Weak.value)).toBe("Hello");
|
|
262
|
+
test2.free();
|
|
263
|
+
expect(test2Weak.value).toBeUndefined();
|
|
264
|
+
allocator.expectNoLeak();
|
|
265
|
+
});
|
|
266
|
+
},
|
|
267
|
+
);
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A holder for an externally ref-counted object.
|
|
3
|
+
*
|
|
4
|
+
* See {@link makeErcType} for how to use
|
|
5
|
+
*/
|
|
6
|
+
export type Erc<TName, TRepr = number> = {
|
|
7
|
+
readonly type: TName;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Underlying object representation.
|
|
11
|
+
*
|
|
12
|
+
* The repr should not be undefinable. undefined means nullptr
|
|
13
|
+
*/
|
|
14
|
+
readonly value: TRepr | undefined;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Free the underlying object.
|
|
18
|
+
*
|
|
19
|
+
* All weak references will be invalidated, and this Erc will become
|
|
20
|
+
* empty
|
|
21
|
+
*/
|
|
22
|
+
free: () => void;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Assign a new value to this Erc.
|
|
26
|
+
*
|
|
27
|
+
* The old value will be freed, and all weak references will be invalidated.
|
|
28
|
+
*/
|
|
29
|
+
assign: (value: TRepr | undefined) => void;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Take the inner value without freeing it.
|
|
33
|
+
*
|
|
34
|
+
* All weak references will be invalidated, and this Erc will become
|
|
35
|
+
* empty
|
|
36
|
+
*/
|
|
37
|
+
take: () => TRepr | undefined;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create a weak reference to the inner value.
|
|
41
|
+
*
|
|
42
|
+
* When this Erc is freed, all weak references will be invalidated.
|
|
43
|
+
*/
|
|
44
|
+
getWeak: () => ErcRef<TName, TRepr>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create a strong reference to the inner value, essentially
|
|
48
|
+
* incrementing the ref count.
|
|
49
|
+
*/
|
|
50
|
+
getStrong: () => Erc<TName, TRepr>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Weak reference to an externally ref-counted object.
|
|
55
|
+
*
|
|
56
|
+
* See {@link makeErcType} for how to use
|
|
57
|
+
*/
|
|
58
|
+
export type ErcRef<TName, TRepr = number> = {
|
|
59
|
+
readonly type: TName;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The underlying object representation.
|
|
63
|
+
*
|
|
64
|
+
* This may become undefined across async calls if the weak reference
|
|
65
|
+
* is invalidated
|
|
66
|
+
*/
|
|
67
|
+
readonly value: TRepr | undefined;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create a strong reference to the inner value, essentially
|
|
71
|
+
* incrementing the ref count.
|
|
72
|
+
*/
|
|
73
|
+
getStrong: () => Erc<TName, TRepr>;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type ErcRefType<T> =
|
|
77
|
+
T extends Erc<infer TName, infer TRepr> ? ErcRef<TName, TRepr> : never;
|
|
78
|
+
|
|
79
|
+
export type ErcTypeConstructor<TName, TRepr> = {
|
|
80
|
+
/**
|
|
81
|
+
* A marker value for the underlying object type.
|
|
82
|
+
*
|
|
83
|
+
* This is commonly a string literal or a symbol.
|
|
84
|
+
*/
|
|
85
|
+
marker: TName;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The function to free the underlying object.
|
|
89
|
+
*/
|
|
90
|
+
free: (value: TRepr) => void;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Given a value, increase the ref count and return the new reference.
|
|
94
|
+
* The returned representation should be a different value if double indirection
|
|
95
|
+
* is used (each value is a pointer to the smart pointer), or the same value
|
|
96
|
+
* if single indirection is used (the value is pointing to the object itself).
|
|
97
|
+
*/
|
|
98
|
+
addRef: (value: TRepr) => TRepr;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create a constructor function that serves as the type for an externally ref-counted
|
|
103
|
+
* object type. Erc instances can then be created for manually managing memory for
|
|
104
|
+
* external objects, typically through FFI.
|
|
105
|
+
*
|
|
106
|
+
* Since JS is garbage collected and has no way to enforce certain memory management,
|
|
107
|
+
* the programmer must ensure that Erc instances are handled correctly!!!
|
|
108
|
+
*
|
|
109
|
+
* ## Defining Erc type
|
|
110
|
+
* ```typescript
|
|
111
|
+
* import { makeErcType, type Erc } from "@pistonite/pure/memory";
|
|
112
|
+
*
|
|
113
|
+
* // 2 functions are required to create an Erc type:
|
|
114
|
+
* // free: free the underlying value (essentially decrementing the ref count)
|
|
115
|
+
* // addRef: increment the ref count and return the new reference
|
|
116
|
+
*
|
|
117
|
+
* // here, assume `number` is the JS type used to represent the external object
|
|
118
|
+
* // for example, this can be a pointer to a C++ object
|
|
119
|
+
* declare function freeFoo(obj: number) => void;
|
|
120
|
+
* declare function addRefFoo(obj: number) => number;
|
|
121
|
+
*
|
|
122
|
+
* // assume another function to create (allocate) the object
|
|
123
|
+
* declare function createFoo(): number;
|
|
124
|
+
*
|
|
125
|
+
* // The recommended way is to create a unique symbol for tracking
|
|
126
|
+
* // the external type, you can also use a string literal
|
|
127
|
+
* const Foo = Symbol("Foo");
|
|
128
|
+
* type Foo = typeof Foo;
|
|
129
|
+
*
|
|
130
|
+
* // now, we can create the Erc type
|
|
131
|
+
* const makeFooErc = makeErcType({
|
|
132
|
+
* marker: Foo,
|
|
133
|
+
* free: freeFoo,
|
|
134
|
+
* addRef: addRefFoo,
|
|
135
|
+
* });
|
|
136
|
+
*
|
|
137
|
+
* // and create Erc instances
|
|
138
|
+
* const myFoo: Erc<Foo> = makeFooErc(createFoo());
|
|
139
|
+
* ```
|
|
140
|
+
*
|
|
141
|
+
* ## Using Erc (strong reference)
|
|
142
|
+
* Each `Erc` instance is a strong reference, corresponding to some ref-counted
|
|
143
|
+
* object externally. Therefore, owner of the `Erc` instance should
|
|
144
|
+
* not expose the `Erc` instance to others (for example, returning it from a function),
|
|
145
|
+
* since it will lead to memory leak or double free.
|
|
146
|
+
* ```typescript
|
|
147
|
+
* // create a foo instance externally, and wrap it with Erc
|
|
148
|
+
* const myFoo = makeFooErc(createFoo());
|
|
149
|
+
*
|
|
150
|
+
* // if ownership of myFoo should be returned to external, use `take()`
|
|
151
|
+
* // this will make myFoo empty, and doSomethingWithFooExternally should free it
|
|
152
|
+
* doSomethingWithFooExternally(myFoo.take());
|
|
153
|
+
*
|
|
154
|
+
* // you can also free it directly
|
|
155
|
+
* foo.free();
|
|
156
|
+
* ```
|
|
157
|
+
*
|
|
158
|
+
* ## Using ErcRef (weak reference)
|
|
159
|
+
* Calling `getWeak` on an `Erc` will return a weak reference that has the same
|
|
160
|
+
* inner value. The weak reference is safe to be passed around and copied.
|
|
161
|
+
* ```typescript
|
|
162
|
+
* const myFooWeak = myFoo.getWeak();
|
|
163
|
+
* ```
|
|
164
|
+
*
|
|
165
|
+
* The weak references are tracked by the `Erc` instance.
|
|
166
|
+
* In the example above, when `myFoo` is freed, all weak references created by
|
|
167
|
+
* `getWeak` will be invalidated. If some other code kept the inner value of
|
|
168
|
+
* the weak reference, it will become a dangling pointer.
|
|
169
|
+
*
|
|
170
|
+
* To avoid this, `getStrong` can be used to create a strong reference if
|
|
171
|
+
* the weak reference is still valid, to ensure that the underlying object
|
|
172
|
+
* is never freed while still in use
|
|
173
|
+
* ```typescript
|
|
174
|
+
* const myFooWeak = myFoo.getWeak();
|
|
175
|
+
*
|
|
176
|
+
* // assume we have some async code that needs to use myFoo
|
|
177
|
+
* declare async function doSomethingWithFoo(foo: FooErcRef): Promise<void>;
|
|
178
|
+
*
|
|
179
|
+
* // Below is BAD!
|
|
180
|
+
* await doSomethingWithFoo(myFooWeak);
|
|
181
|
+
* // Reason: doSomethingWithFoo is async, so it's possible that
|
|
182
|
+
* // myFooWeak is invalidated when it's still needed. If the implementation
|
|
183
|
+
* // does not check for that, it could be deferencing a dangling pointer
|
|
184
|
+
* // (of course, it could actually be fine depending on the implementation of doSomethingWithFoo)
|
|
185
|
+
*
|
|
186
|
+
* // Recommendation is to use strong reference for async operations
|
|
187
|
+
* const myFooStrong = myFooWeak.getStrong();
|
|
188
|
+
* await doSomethingWithFoo(myFooStrong.getWeak()); // will never be freed while awaiting
|
|
189
|
+
* // now we free
|
|
190
|
+
* myFooStrong.free();
|
|
191
|
+
*
|
|
192
|
+
* ```
|
|
193
|
+
*
|
|
194
|
+
* ## Assigning to Erc
|
|
195
|
+
* Each `Erc` instance should only ever have one external reference. So you should not
|
|
196
|
+
* assign to an `Erc` variable directly:
|
|
197
|
+
* ```typescript
|
|
198
|
+
* // DO NOT DO THIS
|
|
199
|
+
* let myFoo = makeFooErc(createFoo());
|
|
200
|
+
* myFoo = makeFooErc(createFoo()); // previous Erc is overriden without proper clean up
|
|
201
|
+
* ```
|
|
202
|
+
*
|
|
203
|
+
* If you want to attach a new value to an existing `Erc`, use the `assign` method:
|
|
204
|
+
* ```typescript
|
|
205
|
+
* const myFoo = makeFooErc(createFoo());
|
|
206
|
+
* myFoo.assign(createFoo()); // previous Erc is freed, and the new one is assigned
|
|
207
|
+
* myFoo.free(); // new one is freed
|
|
208
|
+
* ```
|
|
209
|
+
* The example above does not cause leaks, since the previous Erc is freed
|
|
210
|
+
*
|
|
211
|
+
* However, if you call assign with the value of another `Erc`, it will cause memory
|
|
212
|
+
* issues:
|
|
213
|
+
* ```typescript
|
|
214
|
+
* // DO NOT DO THIS
|
|
215
|
+
* const myFoo1 = makeFooErc(createFoo());
|
|
216
|
+
* const myFoo2 = makeFooErc(createFoo());
|
|
217
|
+
* myFoo1.assign(myFoo2.value); // myFoo1 is freed, and myFoo2 is assigned
|
|
218
|
+
* // BAD: now both myFoo1 and myFoo2 references the same object, but the ref count is 1
|
|
219
|
+
* myFoo1.free(); // no issue here, object is freed, but myFoo2 now holds a dangling pointer
|
|
220
|
+
* myFoo2.free(); // double free!
|
|
221
|
+
*
|
|
222
|
+
* // The correct way to do this is to use `take`:
|
|
223
|
+
* const myFoo1 = makeFooErc(createFoo());
|
|
224
|
+
* const myFoo2 = makeFooErc(createFoo());
|
|
225
|
+
* myFoo1.assign(myFoo2.take()); // myFoo1 is freed, and myFoo2 is assigned, myFoo2 is empty
|
|
226
|
+
* myFoo1.free(); // no issue here, object is freed
|
|
227
|
+
* // myFoo2 is empty, so calling free() has no effect
|
|
228
|
+
* ```
|
|
229
|
+
*
|
|
230
|
+
* Assign also works if both `Erc` are 2 references of the same object:
|
|
231
|
+
* ```typescript
|
|
232
|
+
* const myFoo1 = makeFooErc(createFoo()); // ref count is 1
|
|
233
|
+
* const myFoo2 = myFoo1.getStrong(); // ref count is 2
|
|
234
|
+
* myFoo1.assign(myFoo2.take()); // frees old value, ref count is 1
|
|
235
|
+
*
|
|
236
|
+
* // This also works:
|
|
237
|
+
* const myFoo1 = makeFooErc(createFoo()); // ref count is 1
|
|
238
|
+
* myFoo1.assign(myFoo1.take()); // take() makes myFoo1 empty, so assign() doesn't free it
|
|
239
|
+
*
|
|
240
|
+
* // DO NOT DO THIS:
|
|
241
|
+
* myFoo1.assign(myFoo1.value); // this will free the value since ref count is 0, and result in a dangling pointer
|
|
242
|
+
* ```
|
|
243
|
+
*
|
|
244
|
+
*/
|
|
245
|
+
export const makeErcType = <TName, TRepr>({
|
|
246
|
+
marker,
|
|
247
|
+
free,
|
|
248
|
+
addRef,
|
|
249
|
+
}: ErcTypeConstructor<TName, TRepr>): ((
|
|
250
|
+
value: TRepr | undefined,
|
|
251
|
+
) => Erc<TName, TRepr>) => {
|
|
252
|
+
const createStrongRef = (value: TRepr | undefined): Erc<TName, TRepr> => {
|
|
253
|
+
let weakRef:
|
|
254
|
+
| (ErcRef<TName, TRepr> & { invalidate: () => void })
|
|
255
|
+
| undefined = undefined;
|
|
256
|
+
const invalidateWeakRef = () => {
|
|
257
|
+
if (!weakRef) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const oldWeakRef = weakRef;
|
|
261
|
+
weakRef = undefined;
|
|
262
|
+
oldWeakRef.invalidate();
|
|
263
|
+
};
|
|
264
|
+
const createWeakRef = (initialValue: TRepr | undefined) => {
|
|
265
|
+
const weak = {
|
|
266
|
+
type: marker,
|
|
267
|
+
value: initialValue,
|
|
268
|
+
invalidate: () => {
|
|
269
|
+
weak.value = undefined;
|
|
270
|
+
},
|
|
271
|
+
getStrong: () => {
|
|
272
|
+
if (weak.value === undefined) {
|
|
273
|
+
return createStrongRef(undefined);
|
|
274
|
+
}
|
|
275
|
+
return createStrongRef(addRef(weak.value));
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
return weak;
|
|
279
|
+
};
|
|
280
|
+
const erc = {
|
|
281
|
+
type: marker,
|
|
282
|
+
value,
|
|
283
|
+
free: () => {
|
|
284
|
+
if (erc.value !== undefined) {
|
|
285
|
+
invalidateWeakRef();
|
|
286
|
+
free(erc.value);
|
|
287
|
+
erc.value = undefined;
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
assign: (newValue: TRepr | undefined) => {
|
|
291
|
+
erc.free();
|
|
292
|
+
erc.value = newValue;
|
|
293
|
+
},
|
|
294
|
+
take: () => {
|
|
295
|
+
invalidateWeakRef();
|
|
296
|
+
const oldValue = erc.value;
|
|
297
|
+
erc.value = undefined;
|
|
298
|
+
return oldValue;
|
|
299
|
+
},
|
|
300
|
+
getWeak: () => {
|
|
301
|
+
if (!weakRef) {
|
|
302
|
+
weakRef = createWeakRef(erc.value);
|
|
303
|
+
}
|
|
304
|
+
return weakRef;
|
|
305
|
+
},
|
|
306
|
+
getStrong: () => {
|
|
307
|
+
if (erc.value === undefined) {
|
|
308
|
+
return createStrongRef(undefined);
|
|
309
|
+
}
|
|
310
|
+
return createStrongRef(addRef(erc.value));
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
return erc;
|
|
314
|
+
};
|
|
315
|
+
return createStrongRef;
|
|
316
|
+
};
|
package/src/memory/index.ts
CHANGED
package/src/result/index.ts
CHANGED
|
@@ -197,19 +197,38 @@ export async function tryAsync<T, E = unknown>(
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
/** Try best effort converting an error to a string */
|
|
200
|
-
export function errstr(e: unknown): string {
|
|
200
|
+
export function errstr(e: unknown, recursing?: boolean): string {
|
|
201
201
|
if (typeof e === "string") {
|
|
202
202
|
return e;
|
|
203
203
|
}
|
|
204
|
-
if (e) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
204
|
+
if (!e) {
|
|
205
|
+
return `${e}`;
|
|
206
|
+
}
|
|
207
|
+
if (typeof e === "object" && "message" in e) {
|
|
208
|
+
if (!recursing) {
|
|
209
|
+
return errstr(e.message, true);
|
|
210
|
+
}
|
|
211
|
+
return `${e.message}`;
|
|
212
|
+
}
|
|
213
|
+
if (typeof e === "object" && "toString" in e) {
|
|
214
|
+
const s = e.toString();
|
|
215
|
+
if (!recursing) {
|
|
216
|
+
return errstr(s, true);
|
|
209
217
|
}
|
|
210
|
-
|
|
211
|
-
|
|
218
|
+
return `${s}`;
|
|
219
|
+
}
|
|
220
|
+
// try less-likely fields
|
|
221
|
+
if (typeof e === "object" && "msg" in e) {
|
|
222
|
+
if (!recursing) {
|
|
223
|
+
return errstr(e.msg, true);
|
|
224
|
+
}
|
|
225
|
+
return `${e.msg}`;
|
|
226
|
+
}
|
|
227
|
+
if (typeof e === "object" && "code" in e) {
|
|
228
|
+
if (!recursing) {
|
|
229
|
+
return `error code: ${errstr(e.code, true)}`;
|
|
212
230
|
}
|
|
231
|
+
return `${e.code}`;
|
|
213
232
|
}
|
|
214
233
|
return `${e}`;
|
|
215
234
|
}
|
package/src/memory/weak.ts
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
export type ExternalWeakRef<TUnderlying, TType> = {
|
|
2
|
-
/**
|
|
3
|
-
* A marker value for the underlying object type.
|
|
4
|
-
*
|
|
5
|
-
* This is commonly a string literal or a symbol.
|
|
6
|
-
*/
|
|
7
|
-
type: TType;
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* The underlying object reference.
|
|
11
|
-
*/
|
|
12
|
-
ref: TUnderlying | undefined;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Free the underlying object.
|
|
16
|
-
*/
|
|
17
|
-
free: () => void;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Update the underlying object reference.
|
|
21
|
-
*
|
|
22
|
-
* If the new reference is the same as the old one, nothing will happen.
|
|
23
|
-
* If the old reference is not undefined, it will be freed.
|
|
24
|
-
*/
|
|
25
|
-
set: (value: TUnderlying | undefined) => void;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export type ExternalWeakRefConstructor<TUnderlying, TType> = {
|
|
29
|
-
/**
|
|
30
|
-
* A marker value for the underlying object type.
|
|
31
|
-
*
|
|
32
|
-
* This is commonly a string literal or a symbol.
|
|
33
|
-
*/
|
|
34
|
-
marker: TType;
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* The function to free the underlying object.
|
|
38
|
-
*/
|
|
39
|
-
free: (obj: TUnderlying) => void;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Create a weak reference type for managing externally memory-managed object. This means
|
|
44
|
-
* the objects needs to be freed manually by the external code.
|
|
45
|
-
*
|
|
46
|
-
* The `marker` option is used to distinguish between different types of weak references
|
|
47
|
-
* with the same underlying representation for the reference.
|
|
48
|
-
*
|
|
49
|
-
* Note that the underlying representation should not be undefined-able!
|
|
50
|
-
*
|
|
51
|
-
* ## Example
|
|
52
|
-
* ```typescript
|
|
53
|
-
* import { makeExternalWeakRefType } from "@pistonite/pure/memory";
|
|
54
|
-
*
|
|
55
|
-
* // assume `number` is the JS type used to represent the external object
|
|
56
|
-
* // for example, this can be a pointer to a C++ object
|
|
57
|
-
* declare function freeFoo(obj: number) => void;
|
|
58
|
-
*
|
|
59
|
-
* // some function that allocates a foo object externally and returns
|
|
60
|
-
* // a reference
|
|
61
|
-
* declare function getFoo(): number;
|
|
62
|
-
*
|
|
63
|
-
* const makeFooRef = makeExternalWeakRefType({
|
|
64
|
-
* marker: "foo",
|
|
65
|
-
* free: (obj) => {
|
|
66
|
-
* freeFoo(obj);
|
|
67
|
-
* }
|
|
68
|
-
* });
|
|
69
|
-
* type FooRef = ReturnType<typeof makeFooRef>;
|
|
70
|
-
*
|
|
71
|
-
* // create a reference to a foo object
|
|
72
|
-
* // now this reference can be passed around in JS,
|
|
73
|
-
* // as long as the ownership model is clear and the owner
|
|
74
|
-
* // remembers to free it
|
|
75
|
-
* const fooRef = makeFooRef(getFoo());
|
|
76
|
-
*
|
|
77
|
-
* // free the foo object when it is no longer needed
|
|
78
|
-
* fooRef.free();
|
|
79
|
-
*
|
|
80
|
-
* ## Updating the reference
|
|
81
|
-
* The `set` method will update the reference and free the old one if exists
|
|
82
|
-
* ```
|
|
83
|
-
* const fooRef = makeFooRef(getFoo());
|
|
84
|
-
* fooRef.set(getFoo()); // the old one will be freed, unless it is the same as the new one
|
|
85
|
-
* ```
|
|
86
|
-
*
|
|
87
|
-
* This has a major pitfall: If the ExternalWeakRef is shared, the new object will be accessible
|
|
88
|
-
* by code that has the old reference. In other words, when the reference is updated, code that
|
|
89
|
-
* already has the old reference will not able to know that it has changed.
|
|
90
|
-
*
|
|
91
|
-
* If this is a problem, you should use this pattern instead:
|
|
92
|
-
* ```typescript
|
|
93
|
-
* // track the "current" valid reference
|
|
94
|
-
* let currentRef = makeFooRef(undefined);
|
|
95
|
-
*
|
|
96
|
-
* export const getFooRef = (): FooRef => {
|
|
97
|
-
* // because of this function, many other places can hold
|
|
98
|
-
* // a valid reference to foo
|
|
99
|
-
* return currentRef;
|
|
100
|
-
* }
|
|
101
|
-
*
|
|
102
|
-
* export const updateFooRef = (newFoo: number): void => {
|
|
103
|
-
* // when updating the reference, we create a new weak ref and free the old one
|
|
104
|
-
* if (currentRef.ref === newFoo) {
|
|
105
|
-
* return; // always need to check if old and new are the same, otherwise we will be freeing the new one
|
|
106
|
-
* }
|
|
107
|
-
* const newRef = makeFooRef(newFoo);
|
|
108
|
-
* currentRef.free();
|
|
109
|
-
* currentRef = newRef;
|
|
110
|
-
*
|
|
111
|
-
* // now other places that hold the old reference will see it's freed
|
|
112
|
-
* }
|
|
113
|
-
* ```
|
|
114
|
-
*/
|
|
115
|
-
export const makeExternalWeakRefType = <TUnderlying, TType>({
|
|
116
|
-
marker,
|
|
117
|
-
free,
|
|
118
|
-
}: ExternalWeakRefConstructor<TUnderlying, TType>) => {
|
|
119
|
-
return (
|
|
120
|
-
obj: TUnderlying | undefined,
|
|
121
|
-
): ExternalWeakRef<TUnderlying, TType> => {
|
|
122
|
-
const weakRefObj = {
|
|
123
|
-
type: marker,
|
|
124
|
-
ref: obj,
|
|
125
|
-
free: () => {
|
|
126
|
-
if (weakRefObj.ref !== undefined) {
|
|
127
|
-
free(weakRefObj.ref);
|
|
128
|
-
}
|
|
129
|
-
weakRefObj.ref = undefined;
|
|
130
|
-
},
|
|
131
|
-
set: (value: TUnderlying | undefined) => {
|
|
132
|
-
if (weakRefObj.ref !== undefined && weakRefObj.ref !== value) {
|
|
133
|
-
free(weakRefObj.ref);
|
|
134
|
-
}
|
|
135
|
-
weakRefObj.ref = value;
|
|
136
|
-
},
|
|
137
|
-
};
|
|
138
|
-
return weakRefObj;
|
|
139
|
-
};
|
|
140
|
-
};
|