Skip to content

Commit 76a1ec0

Browse files
committed
generate sync/async
1 parent 10be501 commit 76a1ec0

File tree

3 files changed

+260
-346
lines changed

3 files changed

+260
-346
lines changed

generate.ts

Lines changed: 81 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const FFI_TYPES_PATH = process.env.FFI_TYPES_PATH || './ts/ffi-types.ts'
88
const DEBUG = process.env.DEBUG === 'true'
99
const ASYNCIFY = process.env.ASYNCIFY === 'true'
1010

11-
// const ASSERT_SYNC_FN = 'assertSync'
11+
const ASSERT_SYNC_FN = 'assertSync'
1212

1313
const INCLUDE_RE = /^#include.*$/gm
1414
const TYPEDEF_RE = /^\s*typedef\s+(.+)$/gm
@@ -72,7 +72,14 @@ function main() {
7272

7373
const MaybeAsync = 'MaybeAsync('
7474

75-
function cTypeToTypescriptType(ctype: string) {
75+
interface ParsedType {
76+
typescript: string
77+
ffi: string | null
78+
ctype: string
79+
async: boolean
80+
}
81+
82+
function cTypeToTypescriptType(ctype: string): ParsedType {
7683
// simplify
7784
let type = ctype
7885
// remove const: ignored in JS
@@ -85,11 +92,10 @@ function cTypeToTypescriptType(ctype: string) {
8592
async = true
8693
type = type.slice(MaybeAsync.length, -1)
8794
}
88-
const maybeAsync = (type: string) => (async && ASYNCIFY ? `${type} | Promise<${type}>` : type)
8995

9096
// mapping
9197
if (type.includes('char*')) {
92-
return { ffi: 'string', typescript: maybeAsync('string'), ctype, async }
98+
return { ffi: 'string', typescript: 'string', ctype, async }
9399
}
94100

95101
let typescript = type.replace(/\*/g, 'Pointer')
@@ -110,7 +116,69 @@ function cTypeToTypescriptType(ctype: string) {
110116
ffi = 'number'
111117
}
112118

113-
return { typescript: maybeAsync(typescript), ffi, ctype, async }
119+
return { typescript: typescript, ffi, ctype, async }
120+
}
121+
122+
function renderFunction(args: {
123+
functionName: string
124+
returnType: ParsedType
125+
params: Array<{ name: string; type: ParsedType }>
126+
async: boolean
127+
}) {
128+
const { functionName, returnType, params, async } = args
129+
const typescriptParams = params
130+
.map(param => {
131+
// Allow JSValue wherever JSValueConst is accepted.
132+
const tsType =
133+
param.type.typescript === 'JSValueConstPointer'
134+
? 'JSValuePointer | JSValueConstPointer'
135+
: param.type.typescript
136+
return `${param.name}: ${tsType}`
137+
})
138+
.join(', ')
139+
140+
const forceSync = ASYNCIFY && !async && returnType.async
141+
const markAsync = async && returnType.async
142+
143+
let typescriptFunctionName = functionName
144+
if (forceSync) {
145+
typescriptFunctionName += '_AssertSync'
146+
} else if (markAsync) {
147+
typescriptFunctionName += '_MaybeAsync'
148+
}
149+
150+
const typescriptReturnType =
151+
async && returnType.async
152+
? `${returnType.typescript} | Promise<${returnType.typescript}>`
153+
: returnType.typescript
154+
const typescriptFunctionType = `(${typescriptParams}) => ${typescriptReturnType}`
155+
156+
const ffiParams = JSON.stringify(params.map(param => param.type.ffi))
157+
const cwrapArgs = [JSON.stringify(functionName), JSON.stringify(returnType.ffi), ffiParams]
158+
if (DEBUG && async) {
159+
// https://emscripten.org/docs/porting/asyncify.html#usage-with-ccall
160+
// Passing {async:true} to cwrap/ccall will wrap all return values in
161+
// Promise.resolve(...), even if the c code doesn't suspend and returns a
162+
// primitive value.
163+
//
164+
// When compiled with -s ASSERTIONS=1, Emscripten will throw if the
165+
// function suspends and {async: true} wasn't passed.
166+
//
167+
// However, we'd like to avoid Promise/async overhead if the call can
168+
// return a primitive value directly. So, we compile in {async:true}
169+
// only in DEBUG mode, where assertions are enabled.
170+
//
171+
// Then we rely on our type system to ensure our code supports both
172+
// primitive and promise-wrapped return values in production mode.
173+
cwrapArgs.push('{ async: true }')
174+
}
175+
176+
let cwrap = `this.module.cwrap(${cwrapArgs.join(', ')})`
177+
if (forceSync) {
178+
cwrap = `${ASSERT_SYNC_FN}(${cwrap})`
179+
}
180+
181+
return ` ${typescriptFunctionName}: ${typescriptFunctionType} =\n ${cwrap}`
114182
}
115183

116184
function buildFFI(matches: RegExpExecArray[]) {
@@ -119,54 +187,19 @@ function buildFFI(matches: RegExpExecArray[]) {
119187
const params = parseParams(rawParams)
120188
return { functionName, returnType: cTypeToTypescriptType(returnType.trim()), params }
121189
})
122-
const decls = parsed.map(fn => {
123-
const typescriptParams = fn.params
124-
.map(param => {
125-
// Allow JSValue wherever JSValueConst is accepted.
126-
const tsType =
127-
param.type.typescript === 'JSValueConstPointer'
128-
? 'JSValuePointer | JSValueConstPointer'
129-
: param.type.typescript
130-
131-
return `${param.name}: ${tsType}`
132-
})
133-
.join(', ')
134-
const typescriptFnType = `(${typescriptParams}) => ${fn.returnType.typescript}`
135-
const ffiParams = JSON.stringify(fn.params.map(param => param.type.ffi))
136-
const cwrapArgs = [
137-
JSON.stringify(fn.functionName),
138-
JSON.stringify(fn.returnType.ffi),
139-
ffiParams,
140-
]
141-
if (ASYNCIFY && DEBUG && fn.returnType.async) {
142-
// https://emscripten.org/docs/porting/asyncify.html#usage-with-ccall
143-
// Passing {async:true} to cwrap/ccall will wrap all return values in
144-
// Promise.resolve(...), even if the c code doesn't suspend and returns a
145-
// primitive value.
146-
//
147-
// When compiled with -s ASSERTIONS=1, Emscripten will throw if the
148-
// function suspends and {async: true} wasn't passed.
149-
//
150-
// However, we'd like to avoid Promise/async overhead if the call can
151-
// return a primitive value directly. So, we compile in {async:true}
152-
// only in DEBUG mode, where assertions are enabled.
153-
//
154-
// Then we rely on our type system to ensure our code supports both
155-
// primitive and promise-wrapped return values in production mode.
156-
cwrapArgs.push('{ async: true }')
190+
const decls: string[] = []
191+
parsed.forEach(fn => {
192+
decls.push(renderFunction({ ...fn, async: false }))
193+
if (fn.returnType.async && ASYNCIFY) {
194+
decls.push(renderFunction({ ...fn, async: true }))
157195
}
158-
let cwrap = `this.module.cwrap(${cwrapArgs.join(', ')})`
159-
// if (DEBUG && ASYNCIFY && !fn.returnType.async) {
160-
// cwrap = `${ASSERT_SYNC_FN}(${cwrap})`
161-
// }
162-
return ` ${fn.functionName}: ${typescriptFnType} =\n ${cwrap}`
163196
})
164197

165198
const ffiTypes = fs.readFileSync(FFI_TYPES_PATH, 'utf-8')
166199
const importFromFfiTypes = matchAll(TS_EXPORT_TYPE_RE, ffiTypes).map(match => match[1])
167-
// if (DEBUG && ASYNCIFY) {
168-
// importFromFfiTypes.push(ASSERT_SYNC_FN)
169-
// }
200+
if (ASYNCIFY) {
201+
importFromFfiTypes.push(ASSERT_SYNC_FN)
202+
}
170203

171204
const ffiClassName = ASYNCIFY ? 'QuickJSAsyncFFI' : 'QuickJSFFI'
172205
const moduleTypeName = ASYNCIFY ? 'QuickJSAsyncEmscriptenModule' : 'QuickJSEmscriptenModule'

0 commit comments

Comments
 (0)