Finish off the rest of the embedding docs.
A first draft of them, at least. They probably need some editing. Remove the "Application Lifecycle" page for now. I do intend to add some docs about how fibers interact with the host app, but I can do that later.
This commit is contained in:
@@ -1,66 +1,366 @@
|
||||
^title Storing C Data
|
||||
|
||||
**TODO: Write these docs.**
|
||||
An embedded language often needs to work with native data. You may want a
|
||||
pointer to some memory managed in the C heap, or maybe you want to store a chunk
|
||||
of data more efficiently than Wren's dynamism allows. Often, you want to have a
|
||||
Wren object that manages an external resource like a file handle.
|
||||
|
||||
<!--
|
||||
For those cases, you can define a **foreign class**, a chimera whose state is
|
||||
half Wren and half C. It is a real Wren class with a name, constructor, and
|
||||
methods. You can define methods on it written in Wren, or [foreign methods][]
|
||||
written in C. It produces real Wren objects that you can pass around, do `is`
|
||||
checks on, etc. But it also wraps a blob of raw memory that is opaque to Wren
|
||||
but accessible from C.
|
||||
|
||||
- foreign classes
|
||||
- embedded language often need to work with native c data
|
||||
- maybe want to refer to pointer to memory in c heap
|
||||
- maybe want more dense efficient encoding c provides
|
||||
- may want to refer to resource otherwise managed outside of wren, like file
|
||||
handle
|
||||
- wrap in foreign class
|
||||
- hybrid of wren and c
|
||||
- foreign class has methods defined in wren (though can be foreign)
|
||||
- each instance is instance of wren class, knows type, etc.
|
||||
- but also stores bytes of data opaque to wren
|
||||
- defining:
|
||||
- "foreign class"
|
||||
- declaring class foreign says "when construct call into c to fill in
|
||||
opaque bytes"
|
||||
- need c fn pointer to do that work
|
||||
- like method, bound when class is declared
|
||||
- bindForeignClassFn
|
||||
- takes two pointers, ctor and finalizer
|
||||
- initializing:
|
||||
- whenever construct instance of foreign class, calls fn
|
||||
- call wrenSetSlotNewForeign() to create instance and tell it how many
|
||||
bytes to allocate
|
||||
- returns void* to opaque data
|
||||
- initialize as see fit
|
||||
- accessing:
|
||||
- data opaque, can't be too opaque, need to use it!
|
||||
- cannot access from within wren, only c
|
||||
- if have instance of foreign class in slot, wrenGetSlotForeign() returns
|
||||
void* to raw data
|
||||
- typically, used if foreign method defined on foreign class
|
||||
- remember receiver in slot zero
|
||||
- freeing:
|
||||
- memory for foreign objects managed like all mem in wren
|
||||
- may be gc'd
|
||||
- usually ok
|
||||
- but foreign obj may point to resource whose lifetime should be tied to
|
||||
life of obj
|
||||
- for ex: file handle
|
||||
- if foreign obj is collected, no longer have way to get to file handle
|
||||
- if didn't close it first, leak
|
||||
- when object is gc'd, want to close file
|
||||
- define finalizer fn
|
||||
- other thing set in bindForeignClassFn
|
||||
- if provided, gc calls this before obj is collected
|
||||
- provides raw bits of obj
|
||||
- nothing else!
|
||||
- called from right inside gc, so vm is in brittle state
|
||||
- don't mess with wren, use stack, etc.
|
||||
- just free resource
|
||||
- cannot fail
|
||||
[foreign methods]: http://localhost:8000/embedding/calling-c-from-wren.html
|
||||
|
||||
-->
|
||||
## Defining a Foreign Class
|
||||
|
||||
Until these are written, you can read the docs in [wren.h][].
|
||||
You define one like so:
|
||||
|
||||
[wren.h]: https://github.com/munificent/wren/blob/master/src/include/wren.h
|
||||
:::wren
|
||||
foreign class Point {
|
||||
// ...
|
||||
}
|
||||
|
||||
The `foreign` keyword tells Wren to loop in the host application when it
|
||||
constructs an instance of the class. The host tells Wren how many bytes of extra
|
||||
memory the foreign instance should contain and in return, Wren gives the host
|
||||
the opportunity to initialize that data.
|
||||
|
||||
To talk to the host app, Wren needs a C function it can call when it constructs
|
||||
an instance of the foreign class. This function is found through a binding
|
||||
process similar to how foreign methods are bound. When you [configure the VM][],
|
||||
you set the `bindForeignClassFn` field in WrenConfiguration to point to a C
|
||||
callback you define. Its signature must be:
|
||||
|
||||
[configure the vm]: configuring-the-vm.html
|
||||
|
||||
:::c
|
||||
WrenForeignClassMethods bindForeignClass(
|
||||
WrenVM* vm, const char* module, const char* className);
|
||||
|
||||
Wren invokes this callback once when a foreign class declaration is executed.
|
||||
Wren passes in the name of the module containing the foreign class, and the name
|
||||
of the class being declared. The host's responsibility is to return one of these
|
||||
structs:
|
||||
|
||||
:::c
|
||||
typedef struct
|
||||
{
|
||||
WrenForeignMethodFn allocate;
|
||||
WrenFinalizerFn finalize;
|
||||
} WrenForeignClassMethods;
|
||||
|
||||
It's a pair of function pointers. The first, `allocate`, is called by Wren
|
||||
whenever an instance of the foreign class is created. (We'll get to the optional
|
||||
`finalize` callback later.) The allocation callback has the same signature as a
|
||||
foreign method:
|
||||
|
||||
:::c
|
||||
void allocate(WrenVM* vm);
|
||||
|
||||
## Initializing an Instance
|
||||
|
||||
When you create an instance of a foreign class by calling one its
|
||||
[constructors][], Wren invokes the `allocate` callback you gave it when binding
|
||||
the foreign class. Your primary responsibility in that callback is to tell Wren
|
||||
how many bytes of raw memory you need. You do that by calling:
|
||||
|
||||
[constructors]: ../classes.html#constructors
|
||||
|
||||
:::c
|
||||
void* wrenSetSlotNewForeign(WrenVM* vm,
|
||||
int slot, int classSlot, size_t size);
|
||||
|
||||
Like other [slot manipulation functions][slot], it both reads from and writes to
|
||||
the slot array. It has a few parameters to make it more general purpose since it
|
||||
can also be used in other foreign methods:
|
||||
|
||||
[slot]: slots-and-handles.html
|
||||
|
||||
* The `slot` parameter is the destination slot where the new foreign object
|
||||
should be placed. When you're calling this in a foreign class's allocate
|
||||
callback, this should be 0.
|
||||
|
||||
* The `classSlot` parameter is the slot where the foreign class being
|
||||
constructed can be found. When the VM calls an allocate callback for a
|
||||
foreign class, the class itself is already in slot 0, so you'll pass 0 for
|
||||
this too.
|
||||
|
||||
* Finally, the `size` parameter is the interesting one. Here, you pass in the
|
||||
number of extra raw bytes of data you want the foreign instance to store.
|
||||
This is the memory you get to play with from C.
|
||||
|
||||
So, for example, if you wanted to create a foreign instance that wraps an extra
|
||||
eight bytes of data, you'd call:
|
||||
|
||||
:::c
|
||||
void* data = wrenSetSlotNewForeign(vm, 0, 0, 8);
|
||||
|
||||
The value returned by `wrenSetSlotNewForeign()` is the raw pointer to the
|
||||
requested bytes. You can cast that to whatever C type makes sense (as long as it
|
||||
fits within the requested number of bytes) and initialize it as you see fit.
|
||||
|
||||
Any parameters passed to the constructor are also available in subsequent slots
|
||||
in the slot array. That way you can initialize the foreign data based on values
|
||||
passed to the constructor from Wren.
|
||||
|
||||
After the allocate callback returns, the class's constructor in Wren is run and
|
||||
execution proceeds like normal. From here on out, within Wren, it appears you
|
||||
have a normal instance of a class. It just happens to have some extra bytes
|
||||
hiding inside it that can be accessed from foreign methods.
|
||||
|
||||
## Accessing Foreign Data
|
||||
|
||||
Typically, the way you make use of the data stored in an instance of a foreign
|
||||
class is through other foreign methods. Those are usually defined on the same
|
||||
foreign class, but can be defined on other classes as well. Wren doesn't care.
|
||||
|
||||
If you call a foreign method on a foreign instance, or you pass in a foreign
|
||||
instance as a parameter to the method, then it will be in the slot array. If the
|
||||
foreign instance is the receiver of the method, it's in slot zero. Parameters go
|
||||
in order after that. Given a foreign instance in a slot, you can access the raw
|
||||
bytes it stores by calling:
|
||||
|
||||
:::c
|
||||
void* wrenGetSlotForeign(WrenVM* vm, int slot);
|
||||
|
||||
You pass in the slot index containing the foreign object and it gives you back a
|
||||
pointer to the raw memory the object wraps. As usual, the C API doesn't do any
|
||||
type or bounds checking, so it's on you to make sure the object in that slot
|
||||
actually *is* an instance of a foreign class and contains as much memory as you
|
||||
access.
|
||||
|
||||
Given that void pointer, you can now freely read and modify the data it points
|
||||
to. They're your bits, Wren just holds them for you.
|
||||
|
||||
## Freeing Resources
|
||||
|
||||
If your foreign instances are just holding memory and you're OK with Wren's
|
||||
garbage collector managing the lifetime of that memory, then you're done. Wren
|
||||
will keep the bytes around as long as their is still a reference to them. When
|
||||
the instance is no longer reachable, eventually the garbage collector will do
|
||||
its thing and free the memory.
|
||||
|
||||
But, often, your foreign data refers to some resource whose lifetime needs to
|
||||
be explicitly managed. For example, if you have a foreign object that wraps an
|
||||
open file handle, you need to ensure that handle doesn't get left open when the
|
||||
GC frees the foreign instance.
|
||||
|
||||
Of course, you can (and usually should) add a method on your foreign class, like
|
||||
`close()` so the user can explicitly release the resource managed by the object.
|
||||
But if they forget to do that and the object is no longer reachable, you want to
|
||||
make sure the resource isn't leaked.
|
||||
|
||||
To that end, you can also provide a *finalizer* function when binding the
|
||||
foreign class. That's the other callback in the WrenForeignClassMethods struct.
|
||||
If you provide that callback, then Wren will invoke it when an instance of your
|
||||
foreign class is about to be freed by the garbage collector. This gives you one
|
||||
last chance to clean up the object's resources.
|
||||
|
||||
Because this is called during the middle of a garbage collection, you do not
|
||||
have unfettered access to the VM. It's not like a normal foreign method where
|
||||
you can monkey around with slots and other stuff. Doing that while the GC is
|
||||
running could leave Wren in a weird state.
|
||||
|
||||
Instead, the finalize callback's signature is only:
|
||||
|
||||
:::c
|
||||
void finalize(void* data);
|
||||
|
||||
Wren gives you the pointer to your foreign function's memory, and that's it. The
|
||||
*only* thing you should do inside a finalizer is release any external resources
|
||||
referenced by that memory.
|
||||
|
||||
## A Full Example
|
||||
|
||||
That's a lot to take in, so let's walk through a full example of a foreign class
|
||||
with a finalizer and a couple of methods. We'll do a File class that wraps the
|
||||
C standard file API.
|
||||
|
||||
In Wren, the class we want looks like this:
|
||||
|
||||
:::wren
|
||||
foreign class File {
|
||||
construct create(path) {}
|
||||
|
||||
foreign write(text)
|
||||
foreign close()
|
||||
}
|
||||
|
||||
So you can create a new file given a path. Once you have one, you can write to
|
||||
it and then explicitly close it if you want. We also need to make sure the file
|
||||
gets closed if the user forgets to and the GC cleans up the object.
|
||||
|
||||
### Setting up the VM
|
||||
|
||||
Over in the host, first we'll set up the VM:
|
||||
|
||||
:::c
|
||||
#include "wren.h"
|
||||
|
||||
int main(int argc, const char* argv[])
|
||||
{
|
||||
WrenConfiguration config;
|
||||
wrenInitConfiguration(&config);
|
||||
|
||||
config.bindForeignClassFn = bindForeignClass;
|
||||
config.bindForeignMethodFn = bindForeignMethod;
|
||||
|
||||
WrenVM* vm = wrenNewVM(&config);
|
||||
wrenInterpret(vm, "some code...");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
### Binding the foreign class
|
||||
|
||||
We give the VM two callbacks. The first is for wiring up the foreign class
|
||||
itself:
|
||||
|
||||
:::c
|
||||
WrenForeignClassMethods bindForeignClass(
|
||||
WrenVM* vm, const char* module, const char* className)
|
||||
{
|
||||
WrenForeignClassMethods methods;
|
||||
|
||||
if (strcmp(className, "File") == 0)
|
||||
{
|
||||
methods->allocate = fileAllocate;
|
||||
methods->finalize = fileFinalize;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Unknown class.
|
||||
methods->allocate = NULL;
|
||||
methods->finalize = NULL;
|
||||
}
|
||||
|
||||
return methods;
|
||||
}
|
||||
|
||||
When our binding callback is invoked for the File class, we return the allocate
|
||||
and finalize functions the VM should call. Allocation looks like:
|
||||
|
||||
:::c
|
||||
#include <stdio.h>
|
||||
#include "wren.h"
|
||||
|
||||
void fileAllocate(WrenVM* vm)
|
||||
{
|
||||
FILE** file = (FILE**)wrenSetSlotNewForeign(vm, 0, 0, sizeof(FILE*));
|
||||
const char* path = wrenGetSlotString(vm, 1);
|
||||
*file = fopen(path, "w");
|
||||
}
|
||||
|
||||
First we create the instance by calling `wrenSetSlotNewForeign()`. We tell it to
|
||||
add enough extra bytes to store a `FILE*` in it, which is C's representation of
|
||||
a file handle. We're given back a pointer to the bytes. Since the file handle is
|
||||
itself a pointer, we end up with a double indirection, hence the `FILE**`. In
|
||||
most cases, you'll just have a single `*`.
|
||||
|
||||
We also pull the file path from the slot array. Then we tell C to create a new
|
||||
file at that path. That gives us back a new file handle -- a `FILE*` -- and we
|
||||
store that back into the foreign instance using `*file`. Now we have a foreign
|
||||
object that wraps an open file handle.
|
||||
|
||||
The finalizer simply casts the foreign instance's data back to the proper type
|
||||
and closes the file:
|
||||
|
||||
:::c
|
||||
void fileFinalize(void* data)
|
||||
{
|
||||
closeFile((FILE**)file);
|
||||
}
|
||||
|
||||
It uses this little utility function:
|
||||
|
||||
:::c
|
||||
static void closeFile(FILE** file)
|
||||
{
|
||||
// Already closed.
|
||||
if (*file == NULL) return;
|
||||
|
||||
fclose(*file);
|
||||
*file = NULL;
|
||||
}
|
||||
|
||||
This closes the file (if it's not already closed) and also nulls out the pointer
|
||||
so that we don't try to use the file after it's been closed.
|
||||
|
||||
### Binding the foreign methods
|
||||
|
||||
That's the foreign *class* part. Now we have a couple of foreign *methods* to
|
||||
handle. The host tells the VM how to find them by giving Wren a pointer to this
|
||||
function:
|
||||
|
||||
:::c
|
||||
WrenForeignMethodFn bindForeignMethod(WrenVM* vm, const char* module,
|
||||
const char* className, bool isStatic, const char* signature)
|
||||
{
|
||||
if (strcmp(className, "File") == 0)
|
||||
{
|
||||
if (!isStatic && strcmp(signature, "write(_)") == 0)
|
||||
{
|
||||
return fileWrite;
|
||||
}
|
||||
|
||||
if (!isStatic && strcmp(signature, "close()") == 0)
|
||||
{
|
||||
return fileClose;
|
||||
}
|
||||
}
|
||||
|
||||
// Unknown method.
|
||||
return NULL;
|
||||
}
|
||||
|
||||
When Wren calls this, we look at the class and method name to figure out which
|
||||
method its binding, and then return a pointer to the appropriate function. The
|
||||
foreign method for writing to the file is:
|
||||
|
||||
:::c
|
||||
void fileWrite(WrenVM* vm)
|
||||
{
|
||||
FILE** file = (FILE**)wrenGetSlotForeign(vm, 0);
|
||||
|
||||
// Make sure the file is still open.
|
||||
if (*file == NULL)
|
||||
{
|
||||
wrenSetSlotString(vm, 0, "Cannot write to a closed file.");
|
||||
wrenAbortFiber(vm, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
const char* text = wrenGetSlotString(vm, 1);
|
||||
fwrite(text, sizeof(char), strlen(text), *file);
|
||||
}
|
||||
|
||||
We use `wrenGetSlotForeign()` to pull the foreign data out of the slot array.
|
||||
Since this method is called on the file itself, the foreign object is in slot
|
||||
zero. We take the resulting pointer and cast it to a pointer of the proper type.
|
||||
Again, because our foreign data is *itself* a pointer, we get a pointer to a
|
||||
pointer.
|
||||
|
||||
We do a little sanity checking to make sure the user isn't writing to a file
|
||||
they already closed. If not, we call `fwrite()` to write to the file.
|
||||
|
||||
The other method is `close()` to let them explicitly close the file:
|
||||
|
||||
void fileClose(WrenVM* vm)
|
||||
{
|
||||
closeFile(file);
|
||||
}
|
||||
|
||||
It uses the same helper we defined above. And that's it, a complete foreign
|
||||
class with a finalizer and a couple of foreign methods. In Wren, you can use it
|
||||
like so:
|
||||
|
||||
:::wren
|
||||
var file = File.create("some/path.txt")
|
||||
file.write("some text")
|
||||
file.close()
|
||||
|
||||
<a class="right" href="configuring-the-vm.html">Configuring the VM →</a>
|
||||
<a href="calling-c-from-wren.html">← Calling C from Wren</a>
|
||||
|
||||
Reference in New Issue
Block a user