///|
/// Controls how generation handles declarations outside MoonBindgen's
/// conservative ABI surface.
pub(all) enum UnsupportedPolicy {
/// Keep supported declarations and record unsupported ones in the report.
Report
/// Reject the complete generation when any unsupported declaration is found.
Error
} derive(Debug, Eq)
///|
#deprecated
pub extend UnsupportedPolicy with Eq::{equal, not_equal}
///|
#deprecated
pub extend UnsupportedPolicy with @debug.Debug::{to_repr}
///|
/// Versioned generation settings parsed from `moonbindgen-config-v1` JSON.
///
/// Use `GenerateConfig::default` for report-only generation, or `parse_config`
/// when loading a user-supplied configuration file.
pub struct GenerateConfig {
schema_version : Int
include_functions : Map[String, Bool]
exclude_functions : Map[String, Bool]
renames : Map[String, String]
type_overrides : Map[String, String]
utf8_inputs : Map[String, Bool]
output_params : Map[String, Bool]
null_inputs : Map[String, Bool]
utf8_borrowed_returns : Map[String, Bool]
utf8_owned_returns : Map[String, String]
unsupported_policy : UnsupportedPolicy
buffers : Array[BufferPolicy]
return_buffers : Array[ReturnBufferPolicy]
multi_outputs : Array[MultiOutputPolicy]
resources : Array[ResourcePolicy]
callbacks : Array[CallbackPolicy]
}
///|
/// A position-based byte buffer policy for a config-v2 C function.
pub struct BufferPolicy {
function_name : String
pointer : Int
length : Int
direction : String
retention : String
copy_parameter : Int
copy_symbol : String
}
///|
/// A borrowed pointer return copied before its owner can be changed.
pub struct ReturnBufferPolicy {
function_name : String
length_function : String
length_args : Array[Int]
null_function : String
null_symbol : String
}
///|
/// A named C output argument, addressed by zero-based parameter position.
pub struct NamedOutput {
index : Int
name : String
}
///|
/// A config-v2 result assembled from multiple explicit C output arguments.
pub struct MultiOutputPolicy {
function_name : String
outputs : Array[NamedOutput]
}
///|
/// A managed opaque handle with a void release function and optional retain.
pub struct ResourcePolicy {
name : String
create : String
release : String
retain : String
views : Array[String]
}
///|
/// A position-based callback contract with an explicit lifetime boundary.
pub struct CallbackPolicy {
function_name : String
callback : Int
lifetime : String
user_data : Int
unregister : String
thread : String
}
///|
/// An error raised when generation configuration is malformed or unsupported.
pub(all) suberror ConfigError {
/// Carries a stable, user-facing validation message.
InvalidConfig(String)
} derive(Debug, Eq)
///|
#deprecated
pub extend ConfigError with Eq::{equal, not_equal}
///|
#deprecated
pub extend ConfigError with @debug.Debug::{to_repr}
///|
/// Returns the default configuration with no filters or overrides and with
/// unsupported declarations reported instead of rejected.
pub fn GenerateConfig::default() -> GenerateConfig {
{
schema_version: 1,
include_functions: Map([]),
exclude_functions: Map([]),
renames: Map([]),
type_overrides: Map([]),
utf8_inputs: Map([]),
output_params: Map([]),
null_inputs: Map([]),
utf8_borrowed_returns: Map([]),
utf8_owned_returns: Map([]),
unsupported_policy: Report,
buffers: [],
return_buffers: [],
multi_outputs: [],
resources: [],
callbacks: [],
}
}
///|
fn invalid_config(message : String) -> Unit raise ConfigError {
raise InvalidConfig(message)
}
///|
fn string_set(
json : Json,
name : String,
) -> Map[String, Bool] raise ConfigError {
let result : Map[String, Bool] = Map([])
match field(json, name) {
None => ()
Some(Array(values)) =>
for value in values {
match value {
String(item) => result[item] = true
_ => invalid_config(name + " must contain only strings")
}
}
_ => invalid_config(name + " must be an array")
}
result
}
///|
fn string_map(
json : Json,
name : String,
) -> Map[String, String] raise ConfigError {
let result : Map[String, String] = Map([])
match field(json, name) {
None => ()
Some(Object(values)) =>
for key, value in values {
match value {
String(item) => result[key] = item
_ => invalid_config(name + " values must be strings")
}
}
_ => invalid_config(name + " must be an object")
}
result
}
///|
fn valid_type_override(name : String) -> Bool {
match name {
"Bool"
| "Byte"
| "Int"
| "UInt"
| "Int64"
| "UInt64"
| "Float"
| "Double" => true
_ => false
}
}
///|
fn valid_c_identifier(name : String) -> Bool {
if name == "" {
return false
}
let mut first = true
for char in name {
if first && !(char.is_ascii_alphabetic() || char == '_') {
return false
}
if !(char.is_ascii_alphabetic() || char.is_ascii_digit() || char == '_') {
return false
}
first = false
}
true
}
///|
fn required_string(json : Json, name : String) -> String raise ConfigError {
match text_field(json, name) {
Some(value) if value != "" => value
_ => {
invalid_config(name + " must be a nonempty string")
""
}
}
}
///|
fn required_index(json : Json, name : String) -> Int raise ConfigError {
match field(json, name) {
Some(Number(number, ..)) if number >= 0.0 &&
number == number.to_int().to_double() => number.to_int()
_ => {
invalid_config(name + " must be a nonnegative integer")
-1
}
}
}
///|
fn optional_string(json : Json, name : String) -> String raise ConfigError {
match field(json, name) {
None => ""
Some(String(value)) => value
_ => {
invalid_config(name + " must be a string")
""
}
}
}
///|
fn parse_buffers(json : Json) -> Array[BufferPolicy] raise ConfigError {
let result : Array[BufferPolicy] = []
let values = match field(json, "buffers") {
None => return result
Some(Array(items)) => items
_ => {
invalid_config("buffers must be an array")
return result
}
}
for item in values {
guard item is Object(fields) else {
invalid_config("buffers entries must be objects")
continue
}
for key, _ in fields {
match key {
"function"
| "pointer"
| "length"
| "direction"
| "retention"
| "copy_parameter"
| "copy_symbol" => ()
_ => invalid_config("unknown buffer field: " + key)
}
}
let function_name = required_string(item, "function")
let pointer = required_index(item, "pointer")
let length = required_index(item, "length")
let direction = required_string(item, "direction")
let retention = match field(item, "retention") {
None => "call"
Some(String(value)) => value
_ => {
invalid_config("retention must be a string")
"call"
}
}
let copy_parameter = match field(item, "copy_parameter") {
None => -1
Some(_) => required_index(item, "copy_parameter")
}
let copy_symbol = optional_string(item, "copy_symbol")
if pointer == length {
invalid_config("buffer pointer and length must differ: " + function_name)
}
if copy_parameter == pointer || copy_parameter == length {
invalid_config("copy parameter conflicts with buffer: " + function_name)
}
if direction != "in" && direction != "out" {
invalid_config("buffer direction must be in or out: " + function_name)
}
if retention != "call" && retention != "copy" {
invalid_config("buffer retention must be call or copy: " + function_name)
}
if direction == "out" && retention != "call" {
invalid_config("output buffers must use call retention: " + function_name)
}
if (copy_parameter >= 0) != (copy_symbol != "") ||
(
copy_parameter >= 0 &&
(
direction != "in" ||
retention != "copy" ||
!valid_c_identifier(copy_symbol)
)
) {
invalid_config("invalid copy token for buffer: " + function_name)
}
for previous in result {
if previous.function_name == function_name {
invalid_config("only one buffer policy per function: " + function_name)
}
}
result.push({
function_name,
pointer,
length,
direction,
retention,
copy_parameter,
copy_symbol,
})
}
result
}
///|
fn parse_return_buffers(
json : Json,
) -> Array[ReturnBufferPolicy] raise ConfigError {
let result : Array[ReturnBufferPolicy] = []
let values = match field(json, "return_buffers") {
None => return result
Some(Array(items)) => items
_ => {
invalid_config("return_buffers must be an array")
return result
}
}
for item in values {
guard item is Object(fields) else {
invalid_config("return_buffers entries must be objects")
continue
}
for key, _ in fields {
match key {
"function"
| "length_function"
| "length_args"
| "null_function"
| "null_symbol" => ()
_ => invalid_config("unknown return buffer field: " + key)
}
}
let function_name = required_string(item, "function")
let length_function = required_string(item, "length_function")
let length_args : Array[Int] = []
match field(item, "length_args") {
Some(Array(items)) =>
for arg in items {
match arg {
Number(number, ..) if number >= 0.0 &&
number == number.to_int().to_double() =>
length_args.push(number.to_int())
_ => invalid_config("length_args must be nonnegative integers")
}
}
_ => invalid_config("length_args must be an array")
}
let null_function = optional_string(item, "null_function")
let null_symbol = optional_string(item, "null_symbol")
if (null_function == "") != (null_symbol == "") ||
(null_symbol != "" && !valid_c_identifier(null_symbol)) {
invalid_config("invalid null discriminator: " + function_name)
}
for previous in result {
if previous.function_name == function_name {
invalid_config("duplicate return buffer: " + function_name)
}
}
result.push({
function_name,
length_function,
length_args,
null_function,
null_symbol,
})
}
result
}
///|
fn parse_multi_outputs(
json : Json,
) -> Array[MultiOutputPolicy] raise ConfigError {
let result : Array[MultiOutputPolicy] = []
let values = match field(json, "multi_outputs") {
None => return result
Some(Array(items)) => items
_ => {
invalid_config("multi_outputs must be an array")
return result
}
}
for item in values {
guard item is Object(fields) else {
invalid_config("multi_outputs entries must be objects")
continue
}
for key, _ in fields {
if key != "function" && key != "outputs" {
invalid_config("unknown multi-output field: " + key)
}
}
let function_name = required_string(item, "function")
let outputs : Array[NamedOutput] = []
match field(item, "outputs") {
Some(Array(entries)) =>
for entry in entries {
guard entry is Object(output_fields) else {
invalid_config("multi-output fields must be objects")
continue
}
for key, _ in output_fields {
if key != "index" && key != "name" {
invalid_config("unknown named output field: " + key)
}
}
let index = required_index(entry, "index")
let name = required_string(entry, "name")
if !valid_c_identifier(name) ||
moonbit_value_name(name) != name ||
name == "status" {
invalid_config("invalid named output field: " + name)
}
for previous in outputs {
if previous.index == index || previous.name == name {
invalid_config("duplicate named output: " + function_name)
}
}
outputs.push({ index, name, })
}
_ => invalid_config("outputs must be an array: " + function_name)
}
if outputs.length() < 2 {
invalid_config(
"multi_outputs requires at least two fields: " + function_name,
)
}
for previous in result {
if previous.function_name == function_name {
invalid_config("duplicate multi-output function: " + function_name)
}
}
result.push({ function_name, outputs, })
}
result
}
///|
fn parse_resources(json : Json) -> Array[ResourcePolicy] raise ConfigError {
let result : Array[ResourcePolicy] = []
let values = match field(json, "resources") {
None => return result
Some(Array(items)) => items
_ => {
invalid_config("resources must be an array")
return result
}
}
for item in values {
guard item is Object(fields) else {
invalid_config("resources entries must be objects")
continue
}
for key, _ in fields {
match key {
"name" | "create" | "release" | "retain" | "views" => ()
_ => invalid_config("unknown resource field: " + key)
}
}
let name = required_string(item, "name")
let create = required_string(item, "create")
let release = required_string(item, "release")
let retain = optional_string(item, "retain")
if !valid_c_identifier(name) ||
moonbit_value_name(name) != name ||
!valid_c_identifier(create) ||
!valid_c_identifier(release) ||
(retain != "" && !valid_c_identifier(retain)) {
invalid_config("invalid resource identifier: " + name)
}
let views : Array[String] = []
match field(item, "views") {
None => ()
Some(Array(items)) =>
for view in items {
match view {
String(value) if valid_c_identifier(value) => {
if views.contains(value) || value == create {
invalid_config("duplicate resource view: " + value)
}
views.push(value)
}
_ => invalid_config("views must contain C function names")
}
}
_ => invalid_config("views must be an array")
}
for previous in result {
if previous.name == name || previous.create == create {
invalid_config("duplicate resource name or create function: " + name)
}
for view in views {
if previous.views.contains(view) ||
previous.create == view ||
create == view {
invalid_config("resource view conflicts with another policy: " + view)
}
}
}
result.push({ name, create, release, retain, views, })
}
result
}
///|
fn parse_callbacks(json : Json) -> Array[CallbackPolicy] raise ConfigError {
let result : Array[CallbackPolicy] = []
let values = match field(json, "callbacks") {
None => return result
Some(Array(items)) => items
_ => {
invalid_config("callbacks must be an array")
return result
}
}
for item in values {
guard item is Object(fields) else {
invalid_config("callbacks entries must be objects")
continue
}
for key, _ in fields {
match key {
"function"
| "callback"
| "lifetime"
| "user_data"
| "unregister"
| "thread" => ()
_ => invalid_config("unknown callback field: " + key)
}
}
let function_name = required_string(item, "function")
let callback = required_index(item, "callback")
let lifetime = required_string(item, "lifetime")
let user_data = if lifetime == "persistent" {
required_index(item, "user_data")
} else {
-1
}
let unregister = if lifetime == "persistent" {
required_string(item, "unregister")
} else {
""
}
let thread = if lifetime == "persistent" {
required_string(item, "thread")
} else {
""
}
if !valid_c_identifier(function_name) ||
(unregister != "" && !valid_c_identifier(unregister)) {
invalid_config("invalid callback function name: " + function_name)
}
if lifetime != "call" && lifetime != "persistent" {
invalid_config("callback lifetime must be call or persistent")
}
if lifetime == "call" &&
(
field(item, "user_data") is Some(_) ||
field(item, "unregister") is Some(_) ||
field(item, "thread") is Some(_)
) {
invalid_config("call-scoped callback cannot declare persistent fields")
}
if lifetime == "persistent" && (thread != "same" || callback == user_data) {
invalid_config(
"persistent callback requires same thread and distinct user_data",
)
}
for previous in result {
if previous.function_name == function_name {
invalid_config("duplicate callback function: " + function_name)
}
}
result.push({
function_name,
callback,
lifetime,
user_data,
unregister,
thread,
})
}
result
}
///|
/// Parses and validates a `moonbindgen-config-v1` JSON document.
///
/// Unknown fields, unsafe type overrides, and schema mismatches raise
/// `ConfigError::InvalidConfig`.
pub fn parse_config(text : String) -> GenerateConfig raise ConfigError {
let json = @json.parse(text) catch {
_ => raise InvalidConfig("configuration is not valid JSON")
}
let fields = match json {
Object(values) => values
_ => {
invalid_config("configuration root must be an object")
return GenerateConfig::default()
}
}
for key, _ in fields {
match key {
"schema"
| "include_functions"
| "exclude_functions"
| "renames"
| "type_overrides"
| "utf8_inputs"
| "output_params"
| "null_inputs"
| "utf8_borrowed_returns"
| "utf8_owned_returns"
| "unsupported_policy"
| "buffers"
| "return_buffers"
| "multi_outputs"
| "resources"
| "callbacks" => ()
_ => invalid_config("unknown configuration field: " + key)
}
}
let schema_version = match text_field(json, "schema") {
Some("moonbindgen-config-v1") => 1
Some("moonbindgen-config-v2") => 2
_ => {
invalid_config(
"schema must be moonbindgen-config-v1 or moonbindgen-config-v2",
)
1
}
}
if schema_version == 1 &&
(
field(json, "buffers") is Some(_) ||
field(json, "return_buffers") is Some(_) ||
field(json, "multi_outputs") is Some(_) ||
field(json, "resources") is Some(_) ||
field(json, "callbacks") is Some(_)
) {
invalid_config("positional policies require moonbindgen-config-v2")
}
let unsupported_policy = match text_field(json, "unsupported_policy") {
None | Some("report") => Report
Some("error") => Error
Some(value) => {
invalid_config(
"unsupported_policy must be report or error, got: " + value,
)
Report
}
}
let type_overrides = string_map(json, "type_overrides")
if schema_version == 2 && type_overrides.length() > 0 {
invalid_config("type_overrides are not permitted in config-v2")
}
for c_type, moon_type in type_overrides {
if !valid_type_override(moon_type) {
invalid_config(
"type_overrides[" + c_type + "] is not a supported C ABI type",
)
}
}
let output_params = string_set(json, "output_params")
let null_inputs = string_set(json, "null_inputs")
let utf8_borrowed_returns = string_set(json, "utf8_borrowed_returns")
let utf8_owned_returns = string_map(json, "utf8_owned_returns")
for name, free_fn in utf8_owned_returns {
if !valid_c_identifier(free_fn) {
invalid_config("utf8_owned_returns[" + name + "] needs a C function name")
}
if utf8_borrowed_returns.contains(name) {
invalid_config("return string ownership conflicts for: " + name)
}
}
for key, _ in output_params {
if null_inputs.contains(key) {
invalid_config("output_params and null_inputs conflict for: " + key)
}
}
{
schema_version,
include_functions: string_set(json, "include_functions"),
exclude_functions: string_set(json, "exclude_functions"),
renames: string_map(json, "renames"),
type_overrides,
utf8_inputs: string_set(json, "utf8_inputs"),
output_params,
null_inputs,
utf8_borrowed_returns,
utf8_owned_returns,
unsupported_policy,
buffers: parse_buffers(json),
return_buffers: parse_return_buffers(json),
multi_outputs: parse_multi_outputs(json),
resources: parse_resources(json),
callbacks: parse_callbacks(json),
}
}
///|
fn GenerateConfig::allows(self : GenerateConfig, name : String) -> Bool {
(
self.include_functions.length() == 0 ||
self.include_functions.contains(name)
) &&
!self.exclude_functions.contains(name)
}
///|
fn GenerateConfig::renamed(self : GenerateConfig, name : String) -> String {
self.renames.get(name).unwrap_or(name)
}
///|
fn GenerateConfig::type_override(
self : GenerateConfig,
raw : String,
canonical : String,
) -> String? {
match self.type_overrides.get(raw) {
Some(value) => Some(value)
None => self.type_overrides.get(canonical)
}
}
///|
fn GenerateConfig::is_utf8_input(
self : GenerateConfig,
function_name : String,
parameter_name : String,
) -> Bool {
self.utf8_inputs.contains(function_name + "." + parameter_name)
}
///|
fn GenerateConfig::is_output(
self : GenerateConfig,
function_name : String,
parameter_name : String,
) -> Bool {
self.output_params.contains(function_name + "." + parameter_name)
}
///|
fn GenerateConfig::is_null_input(
self : GenerateConfig,
function_name : String,
parameter_name : String,
) -> Bool {
self.null_inputs.contains(function_name + "." + parameter_name)
}
///|
fn GenerateConfig::has_bridge(self : GenerateConfig, name : String) -> Bool {
if self.utf8_borrowed_returns.contains(name) ||
self.utf8_owned_returns.contains(name) {
return true
}
for key, _ in self.output_params {
if key.has_prefix(name + ".") {
return true
}
}
for policy in self.buffers {
if policy.function_name == name {
return true
}
}
for policy in self.return_buffers {
if policy.function_name == name {
return true
}
}
for policy in self.multi_outputs {
if policy.function_name == name {
return true
}
}
for policy in self.resources {
if policy.create == name || policy.views.contains(name) {
return true
}
}
for policy in self.callbacks {
if policy.function_name == name {
return true
}
}
false
}
///|
fn GenerateConfig::buffer_for(
self : GenerateConfig,
name : String,
) -> BufferPolicy? {
for policy in self.buffers {
if policy.function_name == name {
return Some(policy)
}
}
None
}
///|
fn GenerateConfig::return_buffer_for(
self : GenerateConfig,
name : String,
) -> ReturnBufferPolicy? {
for policy in self.return_buffers {
if policy.function_name == name {
return Some(policy)
}
}
None
}
///|
fn GenerateConfig::multi_output_for(
self : GenerateConfig,
name : String,
) -> MultiOutputPolicy? {
for policy in self.multi_outputs {
if policy.function_name == name {
return Some(policy)
}
}
None
}
///|
fn GenerateConfig::resource_create_for(
self : GenerateConfig,
name : String,
) -> ResourcePolicy? {
for policy in self.resources {
if policy.create == name {
return Some(policy)
}
}
None
}
///|
fn GenerateConfig::resource_view_for(
self : GenerateConfig,
name : String,
) -> ResourcePolicy? {
for policy in self.resources {
if policy.views.contains(name) {
return Some(policy)
}
}
None
}
///|
fn GenerateConfig::callback_for(
self : GenerateConfig,
name : String,
) -> CallbackPolicy? {
for policy in self.callbacks {
if policy.function_name == name {
return Some(policy)
}
}
None
}