restricted c header imports

This commit is contained in:
2026-06-14 14:42:38 +02:00
parent 4e860b033e
commit 638ca57f5c
24 changed files with 1377 additions and 45 deletions
+169 -5
View File
@@ -3,6 +3,7 @@ package main
import compiler_core "./compiler"
import "./compiler/ast"
import "./compiler/backend"
import "./compiler/cimport"
import "./compiler/checker"
import "./compiler/hir"
import "./compiler/ir"
@@ -18,6 +19,7 @@ import "./compiler/target"
import "./compiler/token"
import "./compiler/types"
import "core:fmt"
import "core:mem"
import "core:os"
import "core:os/os2"
import "core:strings"
@@ -395,20 +397,26 @@ cli_parses_ordered_link_options_and_rejects_invalid_forms :: proc(t: ^testing.T)
options, valid := parse_cli_args([]string{
"brolang",
"app",
"--link",
"--c-link",
"native.c",
"-o",
"app.out",
"--library-path",
"--c-library-path",
"vendor/lib",
"--library",
"--c-library",
"thing",
"--link",
"--c-link",
"helper.o",
"--c-include-path",
"vendor/include",
"--c-define",
"FEATURE=1",
"--target",
"aarch64-macos",
})
defer delete(options.link_arguments)
defer delete(options.c_options.include_paths)
defer delete(options.c_options.defines)
testing.expect(t, valid)
testing.expect_value(t, options.input_path, "app")
@@ -419,6 +427,8 @@ cli_parses_ordered_link_options_and_rejects_invalid_forms :: proc(t: ^testing.T)
testing.expect_value(t, options.link_arguments[1].kind, linker.Kind.Library_Path)
testing.expect_value(t, options.link_arguments[2].kind, linker.Kind.Library)
testing.expect_value(t, options.link_arguments[3].value, "helper.o")
testing.expect_value(t, options.c_options.include_paths[0], "vendor/include")
testing.expect_value(t, options.c_options.defines[0], "FEATURE=1")
testing.expect_value(t, target.name(options.target), "aarch64-macos")
_, unknown_valid := parse_cli_args([]string{"brolang", "app", "-o", "out", "--unknown", "value"})
@@ -426,11 +436,17 @@ cli_parses_ordered_link_options_and_rejects_invalid_forms :: proc(t: ^testing.T)
_, duplicate_output_valid := parse_cli_args([]string{"brolang", "app", "-o", "one", "-o", "two"})
_, duplicate_empty_output_valid := parse_cli_args([]string{"brolang", "app", "-o", "", "-o", "two"})
_, invalid_target := parse_cli_args([]string{"brolang", "app", "-o", "out", "--target", "x86_64-linux"})
_, legacy_link := parse_cli_args([]string{"brolang", "app", "-o", "out", "--link", "native.c"})
_, legacy_library_path := parse_cli_args([]string{"brolang", "app", "-o", "out", "--library-path", "vendor/lib"})
_, legacy_library := parse_cli_args([]string{"brolang", "app", "-o", "out", "--library", "thing"})
testing.expect(t, !unknown_valid)
testing.expect(t, !incomplete_valid)
testing.expect(t, !duplicate_output_valid)
testing.expect(t, !duplicate_empty_output_valid)
testing.expect(t, !invalid_target)
testing.expect(t, !legacy_link)
testing.expect(t, !legacy_library_path)
testing.expect(t, !legacy_library)
}
@(test)
@@ -1319,6 +1335,44 @@ foreign_function_links_from_c_source :: proc(t: ^testing.T) {
testing.expect_value(t, state.exit_code, 42)
}
@(test)
restricted_c_header_imports_compile_and_link :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-header-import"
defer _ = os.remove(output)
arguments := []linker.Argument{{kind=.Input, value="examples/interop/header/native.c"}}
c_options := cimport.Options{
include_paths=[]string{"examples/interop/header/include"},
defines=[]string{"BROLANG_FEATURE"},
}
status := compiler_core.compile_package("examples/interop/header/app", output, arguments, target.DEFAULT, c_options)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
unsupported_c_header_members_diagnose_only_when_referenced :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-header-unsupported"
defer _ = os.remove(output)
c_options := cimport.Options{include_paths=[]string{"examples/interop/header/include"}}
status := compiler_core.compile_package("examples/interop/header_unsupported", output, nil, target.DEFAULT, c_options)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect(t, !state.success)
}
@(test)
compatible_c_header_redeclarations_share_one_llvm_declaration :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-header-duplicate"
defer _ = os.remove(output)
arguments := []linker.Argument{{kind=.Input, value="examples/interop/header/native.c"}}
c_options := cimport.Options{include_paths=[]string{"examples/interop/header/include"}}
status := compiler_core.compile_package("examples/interop/header_duplicate", output, arguments, target.DEFAULT, c_options)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
interop_foundation_matches_zig_compiled_apple_silicon_c_fixture :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-interop-foundation"
@@ -1570,7 +1624,11 @@ backend_translates_link_arguments_without_reordering_them :: proc(t: ^testing.T)
{kind=.Library, value="thing"},
{kind=.Input, value="helper.o"},
}
command := backend.build_command("module.ll", "program", arguments)
c_options := cimport.Options{
include_paths=[]string{"vendor/include"},
defines=[]string{"FEATURE=1"},
}
command := backend.build_command("module.ll", "program", arguments, target.DEFAULT, c_options)
defer backend.destroy_command(command)
expected := []string{
@@ -1580,7 +1638,10 @@ backend_translates_link_arguments_without_reordering_them :: proc(t: ^testing.T)
"-target",
"aarch64-macos",
"-Wno-override-module",
"-Wno-unused-command-line-argument",
"module.ll",
"-Ivendor/include",
"-DFEATURE=1",
"native.c",
"-Lvendor/lib",
"-lthing",
@@ -1594,6 +1655,109 @@ backend_translates_link_arguments_without_reordering_them :: proc(t: ^testing.T)
}
}
Fake_Cimport_State :: struct {
calls: int,
available: bool,
infrastructure: bool,
saw_options: bool,
}
fake_cimport_backend :: proc(user_data: rawptr, request: cimport.Request, allocator: mem.Allocator) -> cimport.Result {
state := (^Fake_Cimport_State)(user_data)
state.calls += 1
state.saw_options =
len(request.include_paths) == 2 &&
request.include_paths[0] == "first/include" &&
request.include_paths[1] == "second/include" &&
len(request.defines) == 2 &&
request.defines[0] == "FIRST=1" &&
request.defines[1] == "SECOND" &&
request.target == target.DEFAULT
result := cimport.init_result(allocator)
if !state.available {
result.infrastructure = state.infrastructure
result.error_message = fmt.aprintf("fake importer unavailable", allocator=allocator)
return result
}
append(&result.types, cimport.Type{kind=.C_Int, child=cimport.INVALID_TYPE})
append(&result.functions, cimport.Function{
name=fmt.aprintf("fake_value", allocator=allocator),
result=cimport.Type_Id(0),
reason=fmt.aprintf("", allocator=allocator),
})
result.available = true
return result
}
@(test)
cimport_backend_is_replaceable :: proc(t: ^testing.T) {
state := Fake_Cimport_State{available=true}
options := cimport.Options{backend={import_header=fake_cimport_backend, user_data=&state}}
result := cimport.import_header(options, "fake.h")
defer cimport.destroy_result(&result)
testing.expect(t, result.available)
testing.expect_value(t, state.calls, 1)
testing.expect_value(t, len(result.functions), 1)
testing.expect_value(t, result.functions[0].name, "fake_value")
}
@(test)
loader_injects_and_caches_cimport_backend_per_compilation :: proc(t: ^testing.T) {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
state := Fake_Cimport_State{available=true}
options := cimport.Options{
include_paths=[]string{"first/include", "second/include"},
defines=[]string{"FIRST=1", "SECOND"},
backend={import_header=fake_cimport_backend, user_data=&state},
}
module, loaded := loader.load(
"examples/interop/header_cache",
&sources,
&diagnostics,
&symbols,
c_options=options,
)
defer ast.destroy_module(&module)
testing.expect(t, loaded)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, state.calls, 1)
testing.expect(t, state.saw_options)
testing.expect_value(t, len(module.imports), 2)
testing.expect_value(t, module.imports[0].target, module.imports[1].target)
testing.expect_value(t, len(module.functions), 2)
}
@(test)
cimport_infrastructure_failure_makes_compilation_unavailable :: proc(t: ^testing.T) {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
state := Fake_Cimport_State{infrastructure=true}
options := cimport.Options{backend={import_header=fake_cimport_backend, user_data=&state}}
module, loaded := loader.load(
"examples/interop/header_cache",
&sources,
&diagnostics,
&symbols,
c_options=options,
)
defer ast.destroy_module(&module)
testing.expect(t, !loaded)
testing.expect_value(t, state.calls, 1)
testing.expect(t, len(diagnostics.items) > 0)
}
@(test)
source_store_owns_buffers_indexes_lines_and_deduplicates_diagnostics :: proc(t: ^testing.T) {
store := source.init_store()