Created
September 16, 2025 23:00
-
-
Save PhoenixIllusion/00f4723f907140f6ba53cf639b1a5ae5 to your computer and use it in GitHub Desktop.
Simple 'roll your own' binary inter-process communication protocol with a class + proxy + meta-data of how to encode/decode parameters limited to Float32 and Int32 arrays/values
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| type ParamType = [0|1, 1|2|3|4]; | |
| export const F4: ParamType = [0,4]; | |
| export const F3: ParamType = [0,3]; | |
| export const F2: ParamType = [0,2]; | |
| export const F1: ParamType = [0,1]; | |
| export const U4: ParamType = [1,4]; | |
| export const U3: ParamType = [1,3]; | |
| export const U2: ParamType = [1,2]; | |
| export const U1: ParamType = [1,1]; | |
| type FunctionMap = Record<string|symbol, Function>; | |
| export type BufferData = [Float32Array, Int32Array, number]; | |
| type FunctionArgs = (number|number[]|Float32Array|Int32Array|Uint32Array)[]; | |
| function isNumber(obj: number|number[]|Float32Array|Int32Array|Uint32Array) { | |
| return typeof obj == 'number'; | |
| } | |
| function writeFunctionIndex(buffer: BufferData, idx: number) { | |
| buffer[1][buffer[2]] = idx; buffer[2]+=1; | |
| } | |
| function writeToBuffer(buffer: BufferData, args: FunctionArgs, meta: MetaData): void { | |
| meta.args.forEach((t,i) => { | |
| const v = args[i]; | |
| const arr = isNumber(v) ? [v] : v; | |
| const buf = buffer[t[0]]; | |
| buf.set(arr, buffer[2]) | |
| buffer[2] += t[1]; | |
| }); | |
| } | |
| function readFunctionIndex(buffer: BufferData): number { | |
| const idx = buffer[1][buffer[2]] | |
| buffer[2]+=1; | |
| return idx; | |
| } | |
| function readFromBuffer(buffer: BufferData, meta: MetaData): FunctionArgs { | |
| const result: FunctionArgs = []; | |
| meta.args.forEach((t) => { | |
| const buf = buffer[t[0]]; | |
| if(t[1] == 1) { | |
| result.push(buf[buffer[2]]) | |
| } else { | |
| result.push(buf.slice(buffer[2], buffer[2]+t[1])) | |
| } | |
| buffer[2] += t[1]; | |
| }); | |
| return result; | |
| } | |
| interface MetaData { | |
| args: ParamType[] | |
| } | |
| const MethodIndexRegistry = new WeakMap<any, Map<string|symbol,number>>(); | |
| const ServiceRegistry = new WeakMap<Function, MetaData>(); | |
| export function fields(meta: MetaData) { | |
| return function <TMethod extends (...args: any[]) => any>( | |
| originalMethod: TMethod, | |
| context: ClassMethodDecoratorContext<ThisParameterType<TMethod>, TMethod> | |
| ) { | |
| if (context.kind !== 'method') { | |
| throw new Error(`@fields can only be applied to methods`); | |
| } | |
| context.addInitializer(function(this: ThisParameterType<TMethod>) { | |
| if(!MethodIndexRegistry.has(this)){ | |
| MethodIndexRegistry.set(this, new Map()); | |
| } | |
| const methodIndex = MethodIndexRegistry.get(this)!; | |
| methodIndex.set(context.name, methodIndex.size) | |
| ServiceRegistry.set(originalMethod, meta); | |
| }) | |
| return originalMethod; | |
| }; | |
| } | |
| function getMethodIndex(target: any, method: string|symbol) { | |
| return MethodIndexRegistry.get(target)?.get(method) ?? -1; | |
| } | |
| function getFields(fn: Function) { | |
| return ServiceRegistry.get(fn); | |
| } | |
| function extractMethodMap<T>(instance: T): FunctionMap { | |
| const proto = Object.getPrototypeOf(instance); | |
| const keys = Object.getOwnPropertyNames(proto).filter(k => k !== 'constructor'); | |
| const map = {} as FunctionMap; | |
| for (const key of keys) { | |
| const fn = (instance as any)[key]; | |
| if (typeof fn === 'function') { | |
| map[key] = fn | |
| } | |
| } | |
| return map; | |
| } | |
| export function makeWrapper<T extends Object>(target: T, buffer: BufferData): T { | |
| const targetMap = extractMethodMap(target); | |
| const $this = new Proxy(target, { | |
| get(_target, p, _receiver) { | |
| const index = getMethodIndex(target, p); | |
| const meta = getFields(targetMap[p]); | |
| if(!meta) throw new Error('Attempted to read field meta data for functions not in registry') | |
| if(index < 0) throw new Error(`Attempted to call method with negative index. Not in registry`) | |
| return (... args: FunctionArgs): T => { | |
| writeFunctionIndex(buffer, index); | |
| writeToBuffer(buffer, args, meta); | |
| return $this; | |
| } | |
| }, | |
| }); | |
| return $this; | |
| } | |
| export function makeDispatch<T>(target: T, buffer: BufferData, dispatch: T) { | |
| const targetMap = extractMethodMap(target); | |
| const dispatchMap = extractMethodMap(dispatch); | |
| const indexMap = MethodIndexRegistry.get(target); | |
| if(!indexMap) throw new Error(`Attempted to make dispatch for target with no method index registry.`) | |
| const metas: MetaData[] = []; | |
| const funcs: Function[] = []; | |
| Array.from(indexMap.entries()).forEach(([k,v]) => { | |
| const funcDispatch = dispatchMap[k]; | |
| if(!funcDispatch) throw new Error(`Failed to wire to dispatch for function ${k.toString()}. Not found`); | |
| const meta = getFields(targetMap[k]); | |
| if(!meta) throw new Error(`No meta data found in target for key ${k.toString()}`); | |
| metas[v] = meta; | |
| funcs[v] = funcDispatch; | |
| }); | |
| return () => { | |
| const index = readFunctionIndex(buffer); | |
| if(index < 0 || !funcs[index]) throw new Error(`Attempted to call method with negative index or not in registry.`) | |
| funcs[index].apply(dispatch, readFromBuffer(buffer, metas[index])); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { quat, vec2 } from 'gl-matrix'; | |
| import { F1, F3, F4, fields, makeDispatch, makeWrapper, U1, U2, type BufferData } from './decorator'; | |
| import './style.css' | |
| const out = document.getElementById('app')!; | |
| function log(... args: any[]) { | |
| out.innerText += JSON.stringify(args.map(x => typeof x == 'number' ? x : Array.from(x)))+'\n'; | |
| } | |
| class TestService { | |
| @fields({ args: [F4, F3]}) | |
| foo(_quat: [number,number,number,number], _position: [number,number,number]) { } | |
| @fields({ args: [F4, U2, F3, F1, U2, U1]}) | |
| test1(_quat: Float32Array, _point: Uint32Array, _vec: [number, number, number], _weight: number, _point2: [number,number], _addr: number) {} | |
| } | |
| class WorkerService extends TestService { | |
| constructor(){super();} | |
| override foo(quat: [number, number, number, number], position: [number, number, number]): void { | |
| log(quat, position); | |
| } | |
| override test1(quat: Float32Array, point: Uint32Array, vec: [number, number, number], weight: number, point2: [number, number], addr: number): void { | |
| log(quat, point, vec, weight, point2, addr); | |
| } | |
| } | |
| const buffer = new ArrayBuffer(400); | |
| const writeBuffer: BufferData = [ new Float32Array(buffer), new Int32Array(buffer), 0]; | |
| const readBuffer: BufferData = [ new Float32Array(buffer), new Int32Array(buffer), 0]; | |
| const service = new TestService(); | |
| const writer = makeWrapper(service, writeBuffer); | |
| const reader = new WorkerService(); | |
| const dispatch = makeDispatch(service, readBuffer, reader); | |
| writer.foo([0,1,0,0],[3,4,5]); | |
| dispatch(); | |
| writer.foo([0,0,0,1],[6,5,4]); | |
| writer.foo([1,0,0,0],[9,5,2]); | |
| dispatch(); | |
| writer.test1( | |
| quat.set(quat.create(), 1,0,1,0) as Float32Array, | |
| new Uint32Array([2,3]), | |
| [0,1,0], 72, vec2.set(vec2.create(), 0.5, -1.75) as [number, number], 23.5 | |
| ) | |
| dispatch(); | |
| dispatch(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment