Skip to main content

wesl_c/
lib.rs

1#![doc = include_str!("../README.md")]
2#![allow(clippy::missing_safety_doc)]
3
4use std::borrow::Cow;
5use std::collections::HashMap;
6use std::ffi::{CStr, CString};
7use std::ops::Deref;
8use std::os::raw::{c_char, c_void};
9use std::ptr;
10
11use wesl::syntax::TranslationUnit;
12use wesl::{ModulePath, ResolveError, Resolver, VirtualResolver, Wesl};
13
14#[cfg(feature = "eval")]
15use wesl::{
16    eval::{Eval, EvalAttrs, Inputs, Instance, RefInstance},
17    syntax::{AccessMode, AddressSpace},
18};
19
20// TODO: this seems unfinished. only wesl_create/destroy_compiler is implemented.
21#[allow(unused)]
22pub struct WeslCompiler {
23    compiler: Wesl<wesl::NoResolver>,
24}
25
26pub struct WeslTranslationUnit {
27    unit: TranslationUnit,
28}
29
30/// cbindgen:rename-all=ScreamingSnakeCase
31#[repr(u8)]
32#[derive(Copy, Clone)]
33pub enum WeslManglerKind {
34    WeslManglerEscape = 0,
35    WeslManglerHash = 1,
36    WeslManglerNone = 2,
37}
38
39/// cbindgen:rename-all=ScreamingSnakeCase
40#[repr(u8)]
41#[derive(Copy, Clone)]
42pub enum WeslBindingType {
43    WeslBindingUniform = 0,
44    WeslBindingStorage = 1,
45    WeslBindingReadOnlyStorage = 2,
46    WeslBindingFiltering = 3,
47    WeslBindingNonFiltering = 4,
48    WeslBindingComparison = 5,
49    WeslBindingFloat = 6,
50    WeslBindingUnfilterableFloat = 7,
51    WeslBindingSint = 8,
52    WeslBindingUint = 9,
53    WeslBindingDepth = 10,
54    WeslBindingWriteOnly = 11,
55    WeslBindingReadWrite = 12,
56    WeslBindingReadOnly = 13,
57}
58
59#[repr(C)]
60#[derive(Copy, Clone)]
61pub struct WeslBinding {
62    pub group: u32,
63    pub binding: u32,
64    pub kind: WeslBindingType,
65    pub data_len: usize,
66    pub data: *const u8,
67}
68
69#[repr(C)]
70pub struct WeslResolveSourceResult {
71    pub success: bool,
72    pub source: *const c_char,
73}
74
75#[repr(C)]
76pub struct WeslResolveModuleResult {
77    pub success: bool,
78    pub module: *mut WeslTranslationUnit,
79}
80
81pub type WeslResolveSourceFunction = unsafe extern "C" fn(
82    path: *const c_char,
83    userdata: *mut c_void,
84) -> *mut WeslResolveSourceResult;
85
86pub type WeslResolveSourceFreeFunction =
87    unsafe extern "C" fn(result: *const WeslResolveSourceResult, userdata: *mut c_void);
88
89pub type WeslResolveModuleFunction = unsafe extern "C" fn(
90    path: *const c_char,
91    userdata: *mut c_void,
92) -> *mut WeslResolveModuleResult;
93pub type WeslResolveModuleFreeFunction =
94    unsafe extern "C" fn(result: *const WeslResolveModuleResult, userdata: *mut c_void);
95
96// Workaround for https://github.com/mozilla/cbindgen/issues/326
97
98pub type WeslResolveModuleFunctionOption = Option<
99    unsafe extern "C" fn(
100        path: *const c_char,
101        userdata: *mut c_void,
102    ) -> *mut WeslResolveModuleResult,
103>;
104pub type WeslResolveModuleFreeFunctionOption =
105    Option<unsafe extern "C" fn(result: *const WeslResolveModuleResult, userdata: *mut c_void)>;
106
107pub type WeslResolveStringFunction =
108    unsafe extern "C" fn(path: *const c_char, userdata: *mut c_void) -> *const c_char;
109pub type WeslResolveFreeStringFunction =
110    unsafe extern "C" fn(result: *const c_char, userdata: *mut c_void);
111
112pub type WeslResolveStringFunctionOption =
113    Option<unsafe extern "C" fn(path: *const c_char, userdata: *mut c_void) -> *const c_char>;
114pub type WeslResolveFreeStringFunctionOption =
115    Option<unsafe extern "C" fn(result: *const c_char, userdata: *mut c_void)>;
116
117#[repr(C)]
118#[derive(Copy, Clone)]
119pub struct WeslResolverOptions {
120    pub userdata: *mut std::ffi::c_void,
121
122    pub resolve_source: WeslResolveSourceFunction,
123    pub resolve_source_free: WeslResolveSourceFreeFunction,
124
125    pub resolve_module: WeslResolveModuleFunctionOption,
126    pub resolve_module_free: WeslResolveModuleFreeFunctionOption,
127
128    pub display_name: WeslResolveStringFunctionOption,
129    pub free_display_name: WeslResolveFreeStringFunctionOption,
130
131    pub fs_path: WeslResolveStringFunctionOption,
132    pub free_fs_path: WeslResolveFreeStringFunctionOption,
133}
134
135#[repr(C)]
136pub struct WeslCompileOptions {
137    pub mangler: WeslManglerKind,
138    pub sourcemap: bool,
139    pub imports: bool,
140    pub condcomp: bool,
141    pub generics: bool,
142    pub strip: bool,
143    pub lower: bool,
144    pub validate: bool,
145    pub naga: bool,
146    pub lazy: bool,
147    pub keep_root: bool,
148    pub mangle_root: bool,
149    pub resolver: *const WeslResolverOptions,
150}
151#[repr(C)]
152pub struct WeslStringMap {
153    pub keys: *const *const c_char,
154    pub values: *const *const c_char,
155    pub len: usize,
156}
157
158#[repr(C)]
159pub struct WeslBoolMap {
160    pub keys: *const *const c_char,
161    pub values: *const bool,
162    pub len: usize,
163}
164#[repr(C)]
165pub struct WeslStringArray {
166    pub items: *const *const c_char,
167    pub len: usize,
168}
169
170#[repr(C)]
171pub struct WeslBindingArray {
172    pub items: *const WeslBinding,
173    pub len: usize,
174}
175
176#[repr(C)]
177pub struct WeslDiagnostic {
178    pub file: *const c_char,
179    pub span_start: usize,
180    pub span_end: usize,
181    pub title: *const c_char,
182}
183
184#[repr(C)]
185pub struct WeslError {
186    pub source: *const c_char,
187    pub message: *const c_char,
188    pub diagnostics: *const WeslDiagnostic,
189    pub diagnostics_len: usize,
190}
191
192#[repr(C)]
193pub struct WeslResult {
194    pub success: bool,
195    pub data: *const c_char,
196    pub error: WeslError,
197}
198
199#[repr(C)]
200pub struct WeslParseResult {
201    pub success: bool,
202    pub data: *const WeslTranslationUnit,
203    pub error: WeslError,
204}
205
206#[repr(C)]
207pub struct WeslExecResult {
208    pub success: bool,
209    pub resources: *const WeslBindingArray,
210    pub error: WeslError,
211}
212
213fn map_mangler_kind(value: WeslManglerKind) -> Option<wesl::ManglerKind> {
214    match value {
215        WeslManglerKind::WeslManglerNone => Some(wesl::ManglerKind::None),
216        WeslManglerKind::WeslManglerHash => Some(wesl::ManglerKind::Hash),
217        WeslManglerKind::WeslManglerEscape => Some(wesl::ManglerKind::Escape),
218    }
219}
220
221// -- helpers
222
223unsafe fn string_map_to_hashmap(map: &WeslStringMap) -> HashMap<String, String> {
224    let mut result = HashMap::new();
225
226    for i in 0..map.len {
227        unsafe {
228            let key_ptr = *map.keys.add(i);
229            let value_ptr = *map.values.add(i);
230
231            if !key_ptr.is_null() && !value_ptr.is_null() {
232                let key = CStr::from_ptr(key_ptr).to_string_lossy().into_owned();
233                let value = CStr::from_ptr(value_ptr).to_string_lossy().into_owned();
234                result.insert(key, value);
235            }
236        }
237    }
238
239    result
240}
241
242unsafe fn bool_map_to_hashmap(map: &WeslBoolMap) -> HashMap<String, bool> {
243    let mut result = HashMap::new();
244
245    for i in 0..map.len {
246        unsafe {
247            let key_ptr = *map.keys.add(i);
248            let value = *map.values.add(i);
249
250            if !key_ptr.is_null() {
251                let key = CStr::from_ptr(key_ptr).to_string_lossy().into_owned();
252                result.insert(key, value);
253            }
254        }
255    }
256
257    result
258}
259
260unsafe fn string_array_to_vec(array: &WeslStringArray) -> Option<Vec<String>> {
261    let mut result = Vec::new();
262
263    for i in 0..array.len {
264        unsafe {
265            let item_ptr = *array.items.add(i);
266            if !item_ptr.is_null() {
267                let item = CStr::from_ptr(item_ptr).to_string_lossy().into_owned();
268                result.push(item);
269            }
270        }
271    }
272
273    Some(result)
274}
275
276fn create_c_string(s: &str) -> *const c_char {
277    match CString::new(s) {
278        Ok(c_str) => {
279            let ptr = c_str.as_ptr();
280            std::mem::forget(c_str);
281            ptr
282        }
283        Err(_) => ptr::null(),
284    }
285}
286
287fn wesl_error_to_c(e: wesl::Error) -> WeslError {
288    let d = wesl::Diagnostic::from(e);
289
290    let diagnostics = if let (Some(span), Some(res)) = (&d.detail.span, &d.detail.module_path) {
291        let diag = WeslDiagnostic {
292            file: create_c_string(&res.components.join("/")),
293            span_start: span.start,
294            span_end: span.end,
295            title: create_c_string(&d.error.to_string()),
296        };
297
298        let boxed = Box::new(diag);
299
300        Box::into_raw(boxed)
301    } else {
302        ptr::null()
303    };
304
305    WeslError {
306        source: d
307            .detail
308            .output
309            .as_ref()
310            .map_or(ptr::null(), |s| create_c_string(s)),
311        message: create_c_string(&d.to_string()),
312        diagnostics,
313        diagnostics_len: if diagnostics.is_null() {
314            0
315        } else {
316            1
317        },
318    }
319}
320
321#[cfg(feature = "eval")]
322unsafe fn binding_array_to_vec(array: &WeslBindingArray) -> Vec<WeslBinding> {
323    let mut result = Vec::new();
324
325    for i in 0..array.len {
326        let binding = unsafe { *array.items.add(i) };
327        result.push(binding);
328    }
329
330    result
331}
332
333#[cfg(feature = "eval")]
334fn parse_c_binding(
335    b: &WeslBinding,
336    wgsl: &wesl::syntax::TranslationUnit,
337) -> Result<((u32, u32), RefInstance), wesl::Error> {
338    let mut ctx = wesl::eval::Context::new(wgsl);
339
340    let ty_expr = wgsl
341        .global_declarations
342        .iter()
343        .find_map(|d| match d.node() {
344            wesl::syntax::GlobalDeclaration::Declaration(d) => {
345                let (group, binding) = d.attr_group_binding(&mut ctx).ok()?;
346                if group == b.group && binding == b.binding {
347                    d.ty.clone()
348                } else {
349                    None
350                }
351            }
352            _ => None,
353        })
354        .ok_or_else(|| {
355            wesl::Error::Custom(format!(
356                "Resource @group({}) @binding({}) not found",
357                b.group, b.binding
358            ))
359        })?;
360
361    let ty = wesl::eval::ty_eval_ty(&ty_expr, &mut ctx)
362        .map_err(|e| wesl::Error::Custom(format!("Failed to evaluate type: {e}")))?;
363
364    let (storage, access) = match b.kind {
365        WeslBindingType::WeslBindingUniform => (AddressSpace::Uniform, AccessMode::Read),
366        WeslBindingType::WeslBindingStorage => (AddressSpace::Storage, AccessMode::ReadWrite),
367        WeslBindingType::WeslBindingReadOnlyStorage => (AddressSpace::Storage, AccessMode::Read),
368        _ => return Err(wesl::Error::Custom("Unsupported binding type".to_string())),
369    };
370
371    let data_slice = unsafe { std::slice::from_raw_parts(b.data, b.data_len) };
372    let inst = Instance::from_buffer(data_slice, &ty).ok_or_else(|| {
373        wesl::Error::Custom(format!(
374            "Resource @group({}) @binding({}) ({} bytes) incompatible with type ({} bytes)",
375            b.group,
376            b.binding,
377            b.data_len,
378            ty.size_of().unwrap_or_default()
379        ))
380    })?;
381
382    Ok((
383        (b.group, b.binding),
384        RefInstance::new(inst, storage, access),
385    ))
386}
387
388#[cfg(feature = "eval")]
389fn create_c_binding_array(bindings: Vec<WeslBinding>) -> *const WeslBindingArray {
390    if bindings.is_empty() {
391        return ptr::null();
392    }
393
394    let items = bindings.into_boxed_slice();
395    let len = items.len();
396    let items_ptr = Box::into_raw(items) as *const WeslBinding;
397
398    let array = Box::new(WeslBindingArray {
399        items: items_ptr,
400        len,
401    });
402    Box::into_raw(array)
403}
404
405// -- main API
406
407#[unsafe(no_mangle)]
408pub unsafe extern "C" fn wesl_create_compiler() -> *mut WeslCompiler {
409    let compiler = Wesl::new_barebones();
410    Box::into_raw(Box::new(WeslCompiler { compiler }))
411}
412
413#[unsafe(no_mangle)]
414pub unsafe extern "C" fn wesl_destroy_compiler(compiler: *mut WeslCompiler) {
415    if !compiler.is_null() {
416        let _ = unsafe { Box::from_raw(compiler) };
417    }
418}
419
420fn error_from_str(s: &str) -> WeslError {
421    WeslError {
422        source: ptr::null(),
423        message: create_c_string(s),
424        diagnostics: ptr::null(),
425        diagnostics_len: 0,
426    }
427}
428
429fn result_from_str(s: &str) -> WeslResult {
430    WeslResult {
431        success: false,
432        data: ptr::null(),
433        error: error_from_str(s),
434    }
435}
436
437fn result_invalid_parameters() -> WeslResult {
438    result_from_str("Invalid parameters")
439}
440
441const NO_ERROR: WeslError = WeslError {
442    source: ptr::null(),
443    message: ptr::null(),
444    diagnostics: ptr::null(),
445    diagnostics_len: 0,
446};
447
448struct CustomResolver {
449    pub options: WeslResolverOptions,
450}
451
452struct FreeGuard<T> {
453    pub data: *const T,
454    pub free_function: unsafe extern "C" fn(*const T, *mut c_void),
455    pub free_userdata: *mut c_void,
456}
457
458impl<T> Drop for FreeGuard<T> {
459    fn drop(&mut self) {
460        unsafe {
461            (self.free_function)(self.data, self.free_userdata);
462        }
463    }
464}
465
466impl<T> Deref for FreeGuard<T> {
467    type Target = T;
468
469    fn deref(&self) -> &Self::Target {
470        unsafe { &*self.data }
471    }
472}
473
474impl FreeGuard<c_char> {
475    unsafe fn c_str(&self) -> &CStr {
476        unsafe { CStr::from_ptr(self.data) }
477    }
478}
479
480fn mod_path_to_cstring(path: &ModulePath) -> CString {
481    let path = path.to_string();
482
483    CString::new(path).expect("Module path contained nul bytes!")
484}
485
486unsafe fn resolver_path_to_string<T, F: FnOnce(Cow<'_, str>) -> T>(
487    path: &ModulePath,
488    transform: F,
489    get_func: Option<WeslResolveStringFunction>,
490    free_func: Option<WeslResolveFreeStringFunction>,
491    userdata: *mut c_void,
492) -> Option<T> {
493    let get_func = get_func?;
494    let free_func = free_func?;
495
496    let cstring = mod_path_to_cstring(path);
497
498    let result = unsafe { get_func(cstring.as_ptr(), userdata) };
499
500    if result.is_null() {
501        return None;
502    }
503
504    let result = FreeGuard {
505        data: result,
506        free_function: free_func,
507        free_userdata: userdata,
508    };
509
510    let result_cstr = unsafe { result.c_str() };
511    let result_str = result_cstr.to_string_lossy();
512
513    let ret_result = transform(result_str);
514
515    Some(ret_result)
516}
517
518/// Helper type to call the base `resolve_module` implementation.
519struct ProxyResolver<'a, T>(&'a T);
520
521impl<T: Resolver> wesl::Resolver for ProxyResolver<'_, T> {
522    fn resolve_source<'a>(&'a self, path: &ModulePath) -> Result<Cow<'a, str>, ResolveError> {
523        self.0.resolve_source(path)
524    }
525}
526
527impl wesl::Resolver for CustomResolver {
528    fn resolve_source<'a>(
529        &'a self,
530        path: &ModulePath,
531    ) -> Result<std::borrow::Cow<'a, str>, ResolveError> {
532        let cstring = mod_path_to_cstring(path);
533
534        let result =
535            unsafe { (self.options.resolve_source)(cstring.as_ptr(), self.options.userdata) };
536
537        if result.is_null() {
538            return Err(ResolveError::Error(
539                wesl::Error::Custom("No value returned from resolver".into()).into(),
540            ));
541        }
542
543        let result = FreeGuard {
544            data: result,
545            free_function: self.options.resolve_source_free,
546            free_userdata: self.options.userdata,
547        };
548
549        if !result.success {
550            // TODO: Better error reporting.
551            return Err(ResolveError::Error(
552                wesl::Error::Custom("Custom resolver failed".into()).into(),
553            ));
554        }
555
556        let result_cstr = unsafe { CStr::from_ptr(result.source) };
557        let result_str = result_cstr.to_str().map_err(|_| {
558            ResolveError::Error(
559                wesl::Error::Custom("Resolved source is not valid UTF-8".into()).into(),
560            )
561        })?;
562
563        Ok(result_str.to_owned().into())
564    }
565
566    fn resolve_module(
567        &self,
568        path: &wesl::ModulePath,
569    ) -> Result<wesl::syntax::TranslationUnit, wesl::ResolveError> {
570        let Some(resolve) = self.options.resolve_module else {
571            // Call base implementation since we don't have one.
572            return ProxyResolver(self).resolve_module(path);
573        };
574
575        let resolve_free = self.options.resolve_module_free.unwrap();
576        let cstring = mod_path_to_cstring(path);
577
578        let result = unsafe { resolve(cstring.as_ptr(), self.options.userdata) };
579
580        let result = FreeGuard {
581            data: result,
582            free_function: resolve_free,
583            free_userdata: self.options.userdata,
584        };
585
586        if !result.success {
587            // TODO: Better error reporting.
588            return Err(ResolveError::Error(
589                wesl::Error::Custom("Custom resolver failed".into()).into(),
590            ));
591        }
592
593        let unit = unsafe { &*result.module };
594        Ok(unit.unit.clone())
595    }
596
597    fn display_name(&self, path: &ModulePath) -> Option<String> {
598        unsafe {
599            resolver_path_to_string(
600                path,
601                |str| str.into_owned(),
602                self.options.display_name,
603                self.options.free_display_name,
604                self.options.userdata,
605            )
606        }
607    }
608
609    fn fs_path(&self, path: &ModulePath) -> Option<std::path::PathBuf> {
610        unsafe {
611            resolver_path_to_string(
612                path,
613                |str| str.deref().into(),
614                self.options.display_name,
615                self.options.free_display_name,
616                self.options.userdata,
617            )
618        }
619    }
620}
621
622fn validate_resolver_options(options: &WeslResolverOptions) -> Result<(), &'static str> {
623    if options.resolve_module.is_none() ^ options.resolve_module_free.is_none() {
624        return Err("resolve_module and resolve_module_free must both be provide if either is");
625    }
626
627    if options.fs_path.is_none() ^ options.free_fs_path.is_none() {
628        return Err("fs_path and free_fs_path must both be provide if either is");
629    }
630
631    if options.display_name.is_none() ^ options.free_display_name.is_none() {
632        return Err("display_name and free_display_name must both be provide if either is");
633    }
634
635    Ok(())
636}
637
638#[unsafe(no_mangle)]
639pub unsafe extern "C" fn wesl_compile(
640    files: Option<&WeslStringMap>,
641    root: *const c_char,
642    options: &WeslCompileOptions,
643    keep: &WeslStringArray,
644    features: &WeslBoolMap,
645) -> WeslResult {
646    if root.is_null() {
647        return result_invalid_parameters();
648    }
649
650    let root_str = unsafe { CStr::from_ptr(root).to_string_lossy() };
651    let keep_vec = unsafe { string_array_to_vec(keep) };
652    let features_map = unsafe { bool_map_to_hashmap(features) };
653
654    let root_path = match root_str.parse() {
655        Ok(path) => path,
656        Err(e) => return result_from_str(&format!("Invalid root path: {e}")),
657    };
658
659    let resolver: Box<dyn wesl::Resolver> = match (files, options.resolver.is_null()) {
660        (Some(files), true) => {
661            let files_map = unsafe { string_map_to_hashmap(files) };
662            let mut resolver = VirtualResolver::new();
663            for (path, source) in files_map {
664                if let Ok(module_path) = path.parse() {
665                    resolver.add_module(module_path, source.into());
666                }
667            }
668
669            Box::new(resolver)
670        }
671        (None, false) => {
672            let resolver_options = unsafe { &*options.resolver };
673            if let Err(msg) = validate_resolver_options(resolver_options) {
674                return result_from_str(msg);
675            }
676
677            Box::new(CustomResolver {
678                options: *resolver_options,
679            })
680        }
681        (Some(_), false) => {
682            return result_from_str("Files and custom resolver cannot be specified at once");
683        }
684        _ => return result_from_str("Files or custom resolver must be specified"),
685    };
686
687    let Some(mangler) = map_mangler_kind(options.mangler) else {
688        return result_from_str("Invalid mangler kind specified");
689    };
690
691    let mut compiler = Wesl::new_barebones().set_custom_resolver(resolver);
692    let compiler = compiler
693        .set_options(wesl::CompileOptions {
694            imports: options.imports,
695            condcomp: options.condcomp,
696            generics: options.generics,
697            strip: options.strip,
698            lower: options.lower,
699            validate: options.validate,
700            lazy: options.lazy,
701            mangle_root: options.mangle_root,
702            keep: keep_vec,
703            features: wesl::Features {
704                default: wesl::Feature::Disable,
705                flags: features_map
706                    .into_iter()
707                    .map(|(k, v)| (k, v.into()))
708                    .collect(),
709            },
710            keep_root: options.keep_root,
711        })
712        .use_sourcemap(options.sourcemap)
713        .set_mangler(mangler);
714
715    match compiler.compile(&root_path) {
716        Ok(result) => {
717            let output = result.to_string();
718            WeslResult {
719                success: true,
720                data: create_c_string(&output),
721                error: NO_ERROR,
722            }
723        }
724        Err(e) => WeslResult {
725            success: false,
726            data: ptr::null(),
727            error: wesl_error_to_c(e),
728        },
729    }
730}
731
732#[unsafe(no_mangle)]
733pub unsafe extern "C" fn wesl_parse(source: *const c_char) -> WeslParseResult {
734    if source.is_null() {
735        return WeslParseResult {
736            success: false,
737            data: ptr::null_mut(),
738            error: error_from_str("Invalid parameters"),
739        };
740    };
741
742    let cstr = unsafe { CStr::from_ptr(source) };
743    let Ok(str) = cstr.to_str() else {
744        return WeslParseResult {
745            success: false,
746            data: ptr::null_mut(),
747            error: error_from_str("Source is not valid UTF-8"),
748        };
749    };
750
751    match str.parse::<TranslationUnit>() {
752        Ok(unit) => {
753            let ptr = Box::into_raw(Box::new(WeslTranslationUnit { unit }));
754            WeslParseResult {
755                success: true,
756                data: ptr,
757                error: NO_ERROR,
758            }
759        }
760        Err(e) => WeslParseResult {
761            success: false,
762            data: ptr::null_mut(),
763            error: wesl_error_to_c(wesl::Error::ParseError(e)),
764        },
765    }
766}
767
768#[cfg(feature = "eval")]
769#[unsafe(no_mangle)]
770pub unsafe extern "C" fn wesl_eval(
771    files: &WeslStringMap,
772    root: *const c_char,
773    expression: *const c_char,
774    options: &WeslCompileOptions,
775    features: Option<&WeslBoolMap>,
776) -> WeslResult {
777    let files_map = unsafe { string_map_to_hashmap(files) };
778    let root_str = unsafe { CStr::from_ptr(root).to_string_lossy() };
779    let expr_str = unsafe { CStr::from_ptr(expression).to_string_lossy() };
780    let features_map = features
781        .map(|features| unsafe { bool_map_to_hashmap(features) })
782        .unwrap_or_default();
783
784    let root_path = match root_str.parse() {
785        Ok(path) => path,
786        Err(e) => {
787            return WeslResult {
788                success: false,
789                data: ptr::null(),
790                error: WeslError {
791                    source: ptr::null(),
792                    message: create_c_string(&format!("Invalid root path: {e}")),
793                    diagnostics: ptr::null(),
794                    diagnostics_len: 0,
795                },
796            };
797        }
798    };
799
800    let mut resolver = VirtualResolver::new();
801    for (path, source) in files_map {
802        if let Ok(module_path) = path.parse() {
803            resolver.add_module(module_path, source.into());
804        }
805    }
806
807    let mut compiler = Wesl::new_barebones().set_custom_resolver(resolver);
808    let compiler = compiler
809        .set_options(wesl::CompileOptions {
810            imports: options.imports,
811            condcomp: options.condcomp,
812            generics: options.generics,
813            strip: options.strip,
814            lower: options.lower,
815            validate: options.validate,
816            lazy: options.lazy,
817            mangle_root: options.mangle_root,
818            keep: None,
819            features: wesl::Features {
820                default: wesl::Feature::Disable,
821                flags: features_map
822                    .into_iter()
823                    .map(|(k, v)| (k, v.into()))
824                    .collect(),
825            },
826            keep_root: options.keep_root,
827        })
828        .use_sourcemap(options.sourcemap)
829        .set_mangler(map_mangler_kind(options.mangler).expect("invalid mangler kind"));
830
831    match compiler.compile(&root_path) {
832        Ok(result) => match result.eval(&expr_str) {
833            Ok(eval_result) => WeslResult {
834                success: true,
835                data: create_c_string(&eval_result.inst.to_string()),
836                error: WeslError {
837                    source: ptr::null(),
838                    message: ptr::null(),
839                    diagnostics: ptr::null(),
840                    diagnostics_len: 0,
841                },
842            },
843            Err(e) => WeslResult {
844                success: false,
845                data: ptr::null(),
846                error: wesl_error_to_c(e),
847            },
848        },
849        Err(e) => WeslResult {
850            success: false,
851            data: ptr::null(),
852            error: wesl_error_to_c(e),
853        },
854    }
855}
856
857#[cfg(not(feature = "eval"))]
858#[unsafe(no_mangle)]
859pub unsafe extern "C" fn wesl_eval(
860    _files: *const WeslStringMap,
861    _root: *const c_char,
862    _expression: *const c_char,
863    _options: *const WeslCompileOptions,
864    _features: *const WeslBoolMap,
865) -> WeslResult {
866    result_from_str("wesl_eval requires the 'eval' feature to be enabled")
867}
868
869#[cfg(feature = "eval")]
870#[unsafe(no_mangle)]
871pub unsafe extern "C" fn wesl_exec(
872    files: &WeslStringMap,
873    root: &c_char,
874    entrypoint: &c_char,
875    options: &WeslCompileOptions,
876    resources: Option<&WeslBindingArray>,
877    overrides: Option<&WeslStringMap>,
878    features: Option<&WeslBoolMap>,
879) -> WeslExecResult {
880    let files_map = unsafe { string_map_to_hashmap(files) };
881    let root_str = unsafe { CStr::from_ptr(root).to_string_lossy() };
882    let entrypoint_str = unsafe { CStr::from_ptr(entrypoint).to_string_lossy() };
883    let resources_vec = resources
884        .map(|resources| unsafe { binding_array_to_vec(resources) })
885        .unwrap_or_default();
886    let overrides_map = overrides
887        .map(|overrides| unsafe { string_map_to_hashmap(overrides) })
888        .unwrap_or_default();
889    let features_map: HashMap<String, bool> = features
890        .map(|features| unsafe { bool_map_to_hashmap(features) })
891        .unwrap_or_default();
892
893    let root_path = match root_str.parse() {
894        Ok(path) => path,
895        Err(e) => {
896            return WeslExecResult {
897                success: false,
898                resources: ptr::null(),
899                error: WeslError {
900                    source: ptr::null(),
901                    message: create_c_string(&format!("Invalid root path: {e}")),
902                    diagnostics: ptr::null(),
903                    diagnostics_len: 0,
904                },
905            };
906        }
907    };
908
909    let mut resolver = VirtualResolver::new();
910    for (path, source) in files_map {
911        if let Ok(module_path) = path.parse() {
912            resolver.add_module(module_path, source.into());
913        }
914    }
915
916    let mut compiler = Wesl::new_barebones().set_custom_resolver(resolver);
917    let compiler = compiler
918        .set_options(wesl::CompileOptions {
919            imports: options.imports,
920            condcomp: options.condcomp,
921            generics: options.generics,
922            strip: options.strip,
923            lower: options.lower,
924            validate: options.validate,
925            lazy: options.lazy,
926            mangle_root: options.mangle_root,
927            keep: None,
928            features: wesl::Features {
929                default: wesl::Feature::Disable,
930                flags: features_map
931                    .into_iter()
932                    .map(|(k, v)| (k, v.into()))
933                    .collect(),
934            },
935            keep_root: options.keep_root,
936        })
937        .use_sourcemap(options.sourcemap)
938        .set_mangler(map_mangler_kind(options.mangler).expect("invalid mangler kind"));
939
940    match compiler.compile(&root_path) {
941        Ok(result) => {
942            // parse resources
943            let parsed_resources: Result<HashMap<(u32, u32), RefInstance>, wesl::Error> =
944                resources_vec
945                    .iter()
946                    .map(|r| parse_c_binding(r, &result.syntax))
947                    .collect();
948
949            let parsed_resources = match parsed_resources {
950                Ok(resources) => resources,
951                Err(e) => {
952                    return WeslExecResult {
953                        success: false,
954                        resources: ptr::null(),
955                        error: wesl_error_to_c(e),
956                    };
957                }
958            };
959
960            // parse overrides
961            let parsed_overrides: Result<HashMap<String, Instance>, wesl::Error> = overrides_map
962                .iter()
963                .map(|(name, expr)| {
964                    let mut ctx = wesl::eval::Context::new(&result.syntax);
965                    let expr = expr.parse::<wesl::syntax::Expression>().map_err(|e| {
966                        wesl::Error::Custom(format!("Failed to parse override expression: {e}"))
967                    })?;
968                    let inst = expr.eval_value(&mut ctx).map_err(|e| {
969                        wesl::Error::Custom(format!("Failed to evaluate override: {e}"))
970                    })?;
971                    Ok((name.clone(), inst))
972                })
973                .collect();
974
975            let parsed_overrides = match parsed_overrides {
976                Ok(overrides) => overrides,
977                Err(e) => {
978                    return WeslExecResult {
979                        success: false,
980                        resources: ptr::null(),
981                        error: wesl_error_to_c(e),
982                    };
983                }
984            };
985
986            // execute
987            let inputs = Inputs::new_zero_initialized();
988            match result.exec(&entrypoint_str, inputs, parsed_resources, parsed_overrides) {
989                Ok(exec_result) => {
990                    // convert resources back to C format
991                    let output_resources: Vec<WeslBinding> = resources_vec
992                        .iter()
993                        .filter_map(|r| {
994                            let resource = exec_result.resource(r.group, r.binding)?;
995                            let inst = resource.read().ok()?.to_owned();
996                            let mut new_binding = *r;
997                            if let Some(buffer) = inst.to_buffer() {
998                                let boxed_data = buffer.into_boxed_slice();
999                                new_binding.data_len = boxed_data.len();
1000                                new_binding.data = Box::into_raw(boxed_data) as *const u8;
1001                            }
1002                            Some(new_binding)
1003                        })
1004                        .collect();
1005
1006                    WeslExecResult {
1007                        success: true,
1008                        resources: create_c_binding_array(output_resources),
1009                        error: WeslError {
1010                            source: ptr::null(),
1011                            message: ptr::null(),
1012                            diagnostics: ptr::null(),
1013                            diagnostics_len: 0,
1014                        },
1015                    }
1016                }
1017                Err(e) => WeslExecResult {
1018                    success: false,
1019                    resources: ptr::null(),
1020                    error: wesl_error_to_c(e),
1021                },
1022            }
1023        }
1024        Err(e) => WeslExecResult {
1025            success: false,
1026            resources: ptr::null(),
1027            error: wesl_error_to_c(e),
1028        },
1029    }
1030}
1031
1032#[cfg(not(feature = "eval"))]
1033#[unsafe(no_mangle)]
1034pub unsafe extern "C" fn wesl_exec(
1035    _files: *const WeslStringMap,
1036    _root: *const c_char,
1037    _entrypoint: *const c_char,
1038    _options: *const WeslCompileOptions,
1039    _resources: *const WeslBindingArray,
1040    _overrides: *const WeslStringMap,
1041    _features: *const WeslBoolMap,
1042) -> WeslExecResult {
1043    WeslExecResult {
1044        success: false,
1045        resources: ptr::null(),
1046        error: error_from_str("wesl_exec requires the 'eval' feature to be enabled"),
1047    }
1048}
1049
1050// -- memory
1051
1052#[unsafe(no_mangle)]
1053pub unsafe extern "C" fn wesl_free_string(ptr: *const c_char) {
1054    if !ptr.is_null() {
1055        let _ = unsafe { CString::from_raw(ptr as *mut c_char) };
1056    }
1057}
1058
1059unsafe fn free_error(error: &WeslError) {
1060    unsafe {
1061        if !error.source.is_null() {
1062            wesl_free_string(error.source);
1063        }
1064
1065        if !error.message.is_null() {
1066            wesl_free_string(error.message);
1067        }
1068
1069        if !error.diagnostics.is_null() {
1070            let diag = &*error.diagnostics;
1071            if !diag.file.is_null() {
1072                wesl_free_string(diag.file);
1073            }
1074            if !diag.title.is_null() {
1075                wesl_free_string(diag.title);
1076            }
1077            let _ = Box::from_raw(error.diagnostics as *mut WeslDiagnostic);
1078        }
1079    }
1080}
1081
1082#[unsafe(no_mangle)]
1083pub unsafe extern "C" fn wesl_free_result(result: *mut WeslResult) {
1084    if !result.is_null() {
1085        unsafe {
1086            let result = &mut *result;
1087
1088            if !result.data.is_null() {
1089                wesl_free_string(result.data);
1090            }
1091        }
1092    }
1093}
1094
1095#[unsafe(no_mangle)]
1096pub unsafe extern "C" fn wesl_free_exec_result(result: *mut WeslExecResult) {
1097    if !result.is_null() {
1098        unsafe {
1099            let result = &mut *result;
1100
1101            if !result.resources.is_null() {
1102                let resources = &*result.resources;
1103
1104                // free each binding
1105                for i in 0..resources.len {
1106                    let binding = *resources.items.add(i);
1107                    if !binding.data.is_null() {
1108                        let _ = Box::from_raw(std::ptr::slice_from_raw_parts_mut(
1109                            binding.data as *mut u8,
1110                            binding.data_len,
1111                        ));
1112                    }
1113                }
1114
1115                let _ = Box::from_raw(std::ptr::slice_from_raw_parts_mut(
1116                    resources.items as *mut WeslBinding,
1117                    resources.len,
1118                ));
1119
1120                let _ = Box::from_raw(result.resources as *mut WeslBindingArray);
1121            }
1122
1123            free_error(&result.error);
1124        }
1125    }
1126}
1127
1128#[unsafe(no_mangle)]
1129pub unsafe extern "C" fn wesl_free_parse_result(result: *mut WeslParseResult) {
1130    if !result.is_null() {
1131        unsafe {
1132            let result = &*result;
1133
1134            free_error(&result.error);
1135        }
1136    }
1137}
1138
1139#[unsafe(no_mangle)]
1140pub unsafe extern "C" fn wesl_free_translation_unit(unit: *mut WeslTranslationUnit) {
1141    if !unit.is_null() {
1142        let _ = unsafe { Box::from_raw(unit) };
1143    }
1144}
1145
1146// -- utility
1147
1148// note: results from this function must not be freed
1149#[unsafe(no_mangle)]
1150pub unsafe extern "C" fn wesl_version() -> *const c_char {
1151    const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "\0");
1152    VERSION.as_ptr() as *const c_char
1153}