Skip to content

Commit c2e2c3c

Browse files
authored
Merge pull request #482 from savi-lang/add/ffi-globals
Add `:ffi global` variable bindings.
2 parents 52c02e2 + 80770dc commit c2e2c3c

File tree

10 files changed

+206
-29
lines changed

10 files changed

+206
-29
lines changed

core/declarators/declarators.savi

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@
389389
:intrinsic
390390
:term filenames NameList
391391

392-
:: Declare a binding to an unsafe foreign functions (FFI), such as a C function.
392+
:: Declare a binding to an unsafe foreign function (FFI), such as a C function.
393393
::
394394
:: It is common to define such bindings on a dedicated private module, which is
395395
:: usually, by convention, named `_FFI` to make it clear at call sites.
@@ -430,12 +430,77 @@
430430
:term ret Type
431431
:optional
432432

433+
:: Declare a binding to an unsafe foreign function (FFI) global variable,
434+
:: such as a C global variable.
435+
::
436+
:: It is common to define such bindings on a dedicated private module, which is
437+
:: usually, by convention, named `_FFI` to make it clear at call sites.
438+
::
439+
:: Because FFI bindings are inherently unsafe, it becomes the job of FFI-using
440+
:: library authors to guarantee safety of the packages they publish.
441+
:: FFI functions should never be exposed directly as a public feature - they
442+
:: should be carefully wrapped in a library-specific way that can guarantee
443+
:: memory safety, concurrency safety, and capability security safety,
444+
:: up to the same high standards as the Savi standard library.
445+
::
446+
:: This can be easier said than done. When in doubt, avoid using FFI bindings
447+
:: and prefer implementing features in pure Savi code where possible, or
448+
:: ask for an FFI library safety review from experienced community members.
449+
::
450+
:: An `:ffi` global declaration uses `var` or `let` and is similar to a
451+
:: `:var` or `:let` declaration in that it binds a getter and optional setter
452+
:: (only for `var`) to a global variable in the foreign library.
453+
:: Use `var` if the global variable is expected to change (either on the Savi
454+
:: side or on the C side), or `let` if it's expected to be set just once.
455+
::
456+
:: WARNING: Savi programs involve actors and are implicitly multi-threaded,
457+
:: so global variables whose values are expected to change during the life
458+
:: of the program are very likely to cause memory safety issues.
459+
:: You should strongly consider using thread-local FFI variables instead
460+
:: (not yet implemented).
461+
:declarator ffi
462+
:intrinsic
463+
:context type
464+
:begins ffi
465+
466+
:keyword global
467+
:term var_or_let enum (var, let)
468+
:term name Name
469+
:term type Type
470+
471+
// TODO: Implement and document this.
472+
:declarator ffi
473+
:intrinsic
474+
:context type
475+
:begins ffi
476+
477+
:keyword thread_local
478+
:term var_or_let enum (var, let)
479+
:term name Name
480+
:term type Type
481+
482+
:: Override the foreign name used for linking this FFI declaration.
483+
::
484+
:: Normally, an FFI declaration uses the same name for C linking that it uses
485+
:: in Savi. But it's possible to use a different link name by declaring it here.
486+
::
487+
:: This can be useful if you want to use a less verbose name in Savi,
488+
:: or if you want to bind multiple Savi `:ffi` declarations to the same C name.
433489
:declarator foreign_name
434490
:intrinsic
435491
:context ffi
436492

437493
:term name Name
438494

495+
:: Specify a C shared library that fulfills this particular FFI declaration.
496+
::
497+
:: If this declaration is used in the program, that shared library will be
498+
:: linked to the final program binary. If it's not used, the library won't be.
499+
::
500+
:: This is useful for situations where the library may only be appropriate
501+
:: to link on some platforms but not others. In such a situation, you can
502+
:: specify the link library here and make sure every invocation of the
503+
:: platform-specific function is guarded by a platform check conditional block.
439504
:declarator link_lib
440505
:intrinsic
441506
:context ffi
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
2 + 2 == 4!
22
4 - 2 == 2!
3+
the magic number is 3
4+
the other magic number is 4.2

spec/integration/run-ffi-link-c-files/src/Main.savi

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,19 @@
22
:ffi_link_c_files (
33
"../vendor/mylib_add.c"
44
"../vendor/mylib_sub.c"
5+
"../vendor/mylib_magic_number.c"
56
)
67

78
:module _FFI
89
:ffi mylib_add(a I32, b I32) I32
910
:ffi mylib_sub(a I32, b I32) I32
11+
:ffi global let mylib_magic_number I32
12+
:ffi global let mylib_other_magic_number F64
13+
:foreign_name mylib_magic_number_2
1014

1115
:actor Main
1216
:new (env)
1317
env.out.print("2 + 2 == \(_FFI.mylib_add(2, 2))!")
1418
env.out.print("4 - 2 == \(_FFI.mylib_sub(4, 2))!")
19+
env.out.print("the magic number is \(_FFI.mylib_magic_number)")
20+
env.out.print("the other magic number is \(_FFI.mylib_other_magic_number)")
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
int mylib_magic_number = 3;
2+
double mylib_magic_number_2 = 4.2;

src/savi/compiler/code_gen.cr

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -817,7 +817,16 @@ class Savi::Compiler::CodeGen
817817

818818
def gen_func_impl(gtype, gfunc, llvm_func)
819819
return gen_intrinsic(gtype, gfunc, llvm_func) if gfunc.func.has_tag?(:compiler_intrinsic)
820-
return gen_ffi_impl(gtype, gfunc, llvm_func) if gfunc.func.has_tag?(:ffi)
820+
821+
if gfunc.func.has_tag?(:ffi)
822+
if gfunc.func.has_tag?(:ffi_global_getter)
823+
return gen_ffi_global_getter_impl(gtype, gfunc, llvm_func)
824+
elsif gfunc.func.has_tag?(:ffi_global_setter)
825+
return gen_ffi_global_setter_impl(gtype, gfunc, llvm_func)
826+
else
827+
return gen_ffi_impl(gtype, gfunc, llvm_func)
828+
end
829+
end
821830

822831
# Fields with no initializer body can be skipped.
823832
return if gfunc.func.has_tag?(:field) && gfunc.func.body.nil?
@@ -902,8 +911,6 @@ class Savi::Compiler::CodeGen
902911
end
903912

904913
def gen_ffi_impl(gtype, gfunc, llvm_func)
905-
llvm_ffi_func = gen_ffi_decl(gfunc)
906-
907914
gen_func_start(llvm_func, gtype, gfunc)
908915

909916
param_count = llvm_func.params.size
@@ -957,12 +964,42 @@ class Savi::Compiler::CodeGen
957964
else
958965
raise NotImplementedError.new(gfunc.calling_convention)
959966
end
967+
end
960968

961-
# And possibly cast the return type, for the same reasons
962-
# that we possibly cast the argument types earlier above.
963-
ret_type = llvm_type_of(gfunc.func.ret.not_nil!, gfunc)
969+
def gen_ffi_global_decl(gfunc)
970+
llvm_type = llvm_type_of(gfunc.func.ret.not_nil!, gfunc)
964971

965-
value
972+
global = @mod.globals.add(llvm_type, gfunc.func.metadata[:ffi_link_name].as(String))
973+
global.linkage = LLVM::Linkage::External
974+
global.global_constant = gfunc.func.has_tag?(:ffi_global_constant)
975+
global.externally_initialized = true # TODO: false and set an initializer if this ffi var has an initializer
976+
global
977+
end
978+
979+
def gen_ffi_global_getter_impl(gtype, gfunc, llvm_func)
980+
gen_func_start(llvm_func, gtype, gfunc)
981+
982+
global = gen_ffi_global_decl(gfunc)
983+
llvm_type = llvm_type_of(gfunc.func.ret.not_nil!, gfunc)
984+
985+
value = @builder.load(llvm_type, global, "#{global.name}.LOAD")
986+
987+
@builder.ret(value)
988+
989+
gen_func_end(gfunc)
990+
end
991+
992+
def gen_ffi_global_setter_impl(gtype, gfunc, llvm_func)
993+
gen_func_start(llvm_func, gtype, gfunc)
994+
995+
global = @mod.globals[gfunc.func.metadata[:ffi_link_name]]
996+
997+
value = llvm_func.params[0]
998+
@builder.store(value, global)
999+
1000+
@builder.ret(value)
1001+
1002+
gen_func_end(gfunc)
9661003
end
9671004

9681005
def gen_intrinsic_cpointer(gtype, gfunc, llvm_func)
@@ -1851,14 +1888,14 @@ class Savi::Compiler::CodeGen
18511888
args,
18521889
arg_exprs,
18531890
arg_frames,
1854-
gfunc.func.has_tag?(:ffi) ? gfunc : nil,
1891+
gfunc.func.has_tag?(:ffi_call) ? gfunc : nil,
18551892
needs_virtual_call,
18561893
use_receiver,
18571894
!cont.nil?,
18581895
)
18591896

18601897
# If this was an FFI call, skip calling-convention-specific handling.
1861-
return result if gfunc.func.has_tag?(:ffi)
1898+
return result if gfunc.func.has_tag?(:ffi_call)
18621899

18631900
case gfunc.calling_convention
18641901
when GenFunc::Simple
@@ -3201,7 +3238,6 @@ class Savi::Compiler::CodeGen
32013238
global.linkage = LLVM::Linkage::Private
32023239
global.initializer = const
32033240
global.global_constant = true
3204-
global.unnamed_addr = true
32053241
global
32063242
end
32073243

src/savi/ext/llvm/lib_llvm.cr

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ lib LibLLVM
1212
fun const_bit_cast = LLVMConstBitCast(value : ValueRef, to_type : TypeRef) : ValueRef
1313
fun set_unnamed_addr = LLVMSetUnnamedAddr(global : ValueRef, is_unnamed_addr : Int32)
1414
fun is_unnamed_addr = LLVMIsUnnamedAddr(global : ValueRef) : Int32
15+
fun set_externally_initialized = LLVMSetExternallyInitialized(global : ValueRef, is_externally_initialized : Int32)
16+
fun is_externally_initialized = LLVMIsExternallyInitialized(global : ValueRef) : Int32
1517
fun parse_bitcode_in_context = LLVMParseBitcodeInContext(context : ContextRef, mem_buf : MemoryBufferRef, out_m : ModuleRef*, out_message : UInt8**) : Int32
1618
fun link_modules = LLVMLinkModules2(dest : ModuleRef, src : ModuleRef) : Int32
1719
fun strip_module_debug_info = LLVMStripModuleDebugInfo(mod : ModuleRef) : Bool

src/savi/ext/llvm/value_methods.cr

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ module LLVM::ValueMethods
77
LibLLVM.is_unnamed_addr(self) != 0
88
end
99

10+
def externally_initialized=(externally_initialized)
11+
LibLLVM.set_externally_initialized(self, externally_initialized ? 1 : 0)
12+
end
13+
14+
def externally_initialized?
15+
LibLLVM.is_externally_initialized(self) != 0
16+
end
17+
1018
def dll_storage_class : LLVM::DLLStorageClass
1119
LibLLVM.get_dll_storage_class(self)
1220
end

src/savi/program.cr

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,10 @@ class Savi::Program
389389
:constructor,
390390
:copies,
391391
:ffi,
392+
:ffi_call,
393+
:ffi_global_constant,
394+
:ffi_global_getter,
395+
:ffi_global_setter,
392396
:field,
393397
:hygienic,
394398
:inline,

src/savi/program/declarator/intrinsic.cr

Lines changed: 69 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -304,25 +304,63 @@ module Savi::Program::Intrinsic
304304

305305
scope.on_body { |body| function.body = body }
306306
when "ffi"
307-
name, params =
308-
AST::Extract.name_and_params(terms["name_and_params"].not_nil!)
307+
if terms["thread_local"]?
308+
raise NotImplementedError.new(":ffi thread_local")
309+
elsif terms["global"]?
310+
name = terms["name"].as(AST::Identifier)
311+
t = terms["type"].as(AST::Term)
309312

310-
scope.current_function = function = Program::Function.new(
311-
AST::Identifier.new("non").from(declare.terms.first),
312-
name,
313-
params,
314-
terms["ret"]?.as(AST::Term?),
315-
)
316-
317-
function.add_tag(:ffi)
318-
function.add_tag(:inline)
319-
function.add_tag(:variadic) if terms["variadic"]?
320-
321-
ffi_link_name = function.ident.value
322-
ffi_link_name = ffi_link_name[0...-1] if ffi_link_name.ends_with?("!")
323-
function.metadata[:ffi_link_name] = ffi_link_name
313+
scope.current_function = function = Program::Function.new(
314+
AST::Identifier.new("non").from(declare.terms.first),
315+
name,
316+
nil,
317+
t,
318+
)
319+
320+
ffi_link_name = function.ident.value
321+
ffi_link_name = ffi_link_name[0...-1] if ffi_link_name.ends_with?("!")
322+
function.metadata[:ffi_link_name] = ffi_link_name
323+
function.add_tag(:ffi)
324+
function.add_tag(:ffi_global_getter)
325+
function.add_tag(:inline)
326+
function.body = nil
327+
328+
if terms["var_or_let"].as(AST::Identifier).value == "let"
329+
function.add_tag(:ffi_global_constant)
330+
else
331+
setter_function = Program::Function.new(
332+
AST::Identifier.new("non").from(declare.terms.first),
333+
AST::Identifier.new("#{name.value}=").from(name),
334+
AST::Group.new("(", [t]).from(t),
335+
t,
336+
)
337+
338+
setter_function.metadata[:ffi_link_name] = ffi_link_name
339+
setter_function.add_tag(:ffi)
340+
setter_function.add_tag(:ffi_global_setter)
341+
setter_function.add_tag(:inline)
342+
setter_function.body = nil
343+
end
344+
else
345+
name, params =
346+
AST::Extract.name_and_params(terms["name_and_params"].not_nil!)
324347

325-
function.body = nil
348+
scope.current_function = function = Program::Function.new(
349+
AST::Identifier.new("non").from(declare.terms.first),
350+
name,
351+
params,
352+
terms["ret"]?.as(AST::Term?),
353+
)
354+
355+
ffi_link_name = function.ident.value
356+
ffi_link_name = ffi_link_name[0...-1] if ffi_link_name.ends_with?("!")
357+
function.metadata[:ffi_link_name] = ffi_link_name
358+
function.add_tag(:ffi)
359+
function.add_tag(:ffi_call)
360+
function.add_tag(:inline)
361+
function.add_tag(:variadic) if terms["variadic"]?
362+
function.body = nil
363+
end
326364
when "let", "var"
327365
type = scope.current_type
328366

@@ -583,9 +621,23 @@ module Savi::Program::Intrinsic
583621
when "foreign_name"
584622
name = terms["name"].as(AST::Identifier)
585623
scope.current_function.metadata[:ffi_link_name] = name.value
624+
625+
if scope.current_function.has_tag?(:ffi_global_getter)
626+
setter_function = scope.current_type.functions.last
627+
if setter_function.has_tag?(:ffi_global_setter)
628+
setter_function.metadata[:ffi_link_name] = name.value
629+
end
630+
end
586631
when "link_lib"
587632
name = terms["name"].as(AST::Identifier)
588633
scope.current_function.metadata[:ffi_link_lib] = name.value
634+
635+
if scope.current_function.has_tag?(:ffi_global_getter)
636+
setter_function = scope.current_type.functions.last
637+
if setter_function.has_tag?(:ffi_global_setter)
638+
setter_function.metadata[:ffi_link_lib] = name.value
639+
end
640+
end
589641
else
590642
raise NotImplementedError.new(declarator.pretty_inspect)
591643
end

src/savi/program/declarator/term_acceptor.cr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ abstract class Savi::Program::Declarator::TermAcceptor
2121
end
2222

2323
def name : String
24-
"_" # we don't care about where we save the term
24+
@keyword
2525
end
2626

2727
def try_accept(term : AST::Term) : AST::Term?

0 commit comments

Comments
 (0)