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:
Bob Nystrom
2017-10-17 08:26:06 -07:00
parent 9661a5b999
commit e2e16dc4a9
8 changed files with 388 additions and 104 deletions

View File

@ -1,9 +0,0 @@
^title Application Lifecycle
**TODO: Write these docs.**
Until these are written, you can read the docs in [wren.h][].
[wren.h]: https://github.com/munificent/wren/blob/master/src/include/wren.h
<a href="configuring-the-vm.html">&larr; Configuring the VM</a>

View File

@ -37,8 +37,10 @@ statement itself is evaluated -- the VM asks the host application for the C
function that should be used for the foreign method.
It does this through the `bindForeignMethodFn` callback you give it when you
first configure the VM. This callback isn't the foreign method itself. It's the
binding function your app uses to *look up* the foreign methods.
first [configure the VM][config]. This callback isn't the foreign method itself.
It's the binding function your app uses to *look up* foreign methods.
[config]: configuring-the-vm.html
Its signature is:
@ -112,8 +114,7 @@ object is in slot zero, and arguments are in consecutive slots after that.
You use the slot API to read those arguments, and then perform whatever work you
want to in C. If you want the foreign method to return a value, place it in slot
zero. that slot, the foreign method will implicitly return the receiver, since
that's what is already in there.) Like so:
zero. Like so:
:::c
void mathAdd(WrenVM* vm)
@ -124,7 +125,9 @@ that's what is already in there.) Like so:
}
While your foreign method is executed, the VM is completely suspended. No other
fibers will run until your foreign method returns.
fibers will run until your foreign method returns. You should *not* try to
resume the VM from within a foreign method by calling `wrenCall()` or
`wrenInterpret()`. The VM is not re-entrant.
This covers foreign behavior, but what about foreign *state*? For that, we need
a foreign *class*...

View File

@ -7,7 +7,7 @@ compiler, but that's still a good bit of work.
It's also not an effective way to communicate. You can't pass arguments to
Wren&mdash;at least, not without doing something nasty like converting them to
literals in a string of source code&mdash;and we can't get a result value back.
literals in a string of source code&mdash;and you can't get a result value back.
`wrenInterpret()` is great for loading code into the VM, but it's not the best
way to execute code that's already been loaded. What we want to do is invoke
@ -26,7 +26,7 @@ C, first we need a few things:
[signature]: ../method-calls.html#signature
* **The receiver to invoke the method on.** It's the receiver's class that
* **The receiver object to invoke the method on.** The receiver's class
determines which method is actually called.
* **The arguments to pass to the method.**

View File

@ -2,7 +2,7 @@
When you create a Wren VM, you tweak it by passing in a pointer to a
WrenConfiguration structure. Since Wren has no global state, you can configure
each VM them differently if your application happens to run multiple.
each VM differently if your application happens to run multiple.
The struct looks like:
@ -20,7 +20,8 @@ The struct looks like:
int heapGrowthPercent;
} WrenConfiguration;
Most fields have useful defaults, which you can (and should) set by calling:
Most fields have useful defaults, which you can (and should) initialize by
calling:
:::c
wrenInitConfiguration(&configuration);
@ -51,7 +52,7 @@ host should return the source code for that module. Memory for the source should
be allocated using the same allocator that the VM uses for other allocation (see
below). Wren will take ownership of the returned string and free it later.
The module loader will only be called once for any given module name. Wren caches
The module loader is only be called once for any given module name. Wren caches
the result internally so subsequent imports of the same module use the
previously loaded code.
@ -63,21 +64,19 @@ If you don't use any `import` statements, you can leave this `NULL`.
### `bindForeignMethodFn`
The callback Wren uses to find a foreign method and bind it to a class. See
[this page][foreign method] for details.
[this page][foreign method] for details. If your application defines no foreign
methods, you can leave this `NULL`.
[foreign method]: /embedding/calling-c-from-wren.html
If your application defines no foreign methods, you can leave this `NULL`.
### `bindForeignClassFn`
The callback Wren uses to find a foreign class and get its foreign methods. See
[this page][foreign class] for details.
[this page][foreign class] for details. If your application defines no foreign
classes, you can leave this `NULL`.
[foreign class]: /embedding/storing-c-data.html
If your application defines no foreign classes, you can leave this `NULL`.
## Diagnostics
These let you wire up some minimal output so you can tell if your code is doing
@ -108,7 +107,7 @@ is:
int line,
const char* message)
The type parameter is one of:
The `type` parameter is one of:
:::c
typedef enum
@ -197,5 +196,4 @@ frequent garbage collections.
If set to zero, the VM uses a default of 50.
<a class="right" href="application-lifecycle.html">Application Lifecycle &rarr;</a>
<a href="storing-c-data.html">&larr; Storing C Data</a>

View File

@ -92,15 +92,10 @@ have to explicitly link to.
[libm]: https://en.wikipedia.org/wiki/C_mathematical_functions#libm
If your program is in C++ but you are linking to the Wren library compiled as C,
then you'll need to handle the calling convention differences like so:
this header handles the differences in calling conventions between C and C++:
:::c
extern "C" {
#include "wren.h"
}
(Wren's source can be compiled as either C or C++, so you can skip this by
compiling the whole thing yourself as C++. Whatever floats your boat.)
#include "wren.hpp"
## Creating a Wren VM
@ -115,7 +110,7 @@ This gives you a basic configuration that has reasonable defaults for
everything. If you don't need to tweak stuff, you can leave it at that. We'll
[learn more][configuration] about what you can configure later.
[configuration]: configuration.html
[configuration]: configuring-the-vm.html
With this ready, you can create the VM:
@ -128,7 +123,7 @@ a WrenVM. You can have multiple Wren VMs running independently of each other
without any problems, even concurrently on different threads.
`wrenNewVM()` stores its own copy of the configuration, so after calling it, you
can discard the `WrenConfiguration` struct you filled in. Now you have a live
can discard the WrenConfiguration struct you filled in. Now you have a live
VM, waiting to run some code!
## Executing Wren code

View File

@ -14,8 +14,8 @@ of the VM. These functions are based on two fundamental concepts: **slots** and
When you want to send data to Wren, read data from it, or generally monkey
around with Wren objects from C, you do so by going through an array of slots.
You can think of it as a shared message board that both the VM and your C code
leave notes on for the other side to process.
Think of it as a shared message board that both the VM and your C code leave
notes on for the other side to process.
The array is zero-based, and each slot can hold a value of any type. It is
dynamically sized, but it's your responsibility to ensure there are enough slots
@ -58,8 +58,9 @@ enough slots for the objects it is sending you.
### Writing slots
Once you have some slots, you can store data in them using a number of functions all named `wrenSetSlot<type>()` where `<type>` is the kind of data.
We'll start with the simple ones:
Once you have some slots, you store data in them using a number of functions all
named `wrenSetSlot<type>()` where `<type>` is the kind of data. We'll start with
the simple ones:
:::c
void wrenSetSlotBool(WrenVM* vm, int slot, bool value);
@ -231,15 +232,13 @@ A handle is an opaque wrapper around an object of any type, but just as
important, it's a *persistent* one. When Wren gives you a pointer to a
WrenHandle, it guarantees that that pointer remains valid. You can keep it
around as long as you want. Even if a garbage collection occurs, Wren will
ensure all of the handle and the objects they wrap are kept safely in memory.
ensure the handle and the object it wraps are kept safely in memory.
Internally, Wren keeps a list of all of the WrenHandles that have been created.
That way, during garbage collection, it can find them all and make sure their
objects aren't freed.
But what if you don't want it to be kept around any more? Since C relies on
manual memory management, WrenHandle does too. When you are done with one, you
must explicitly release it by calling:
objects aren't freed. But what if you don't want it to be kept around any more?
Since C relies on manual memory management, WrenHandle does too. When you are
done with one, you must explicitly release it by calling:
:::c
void wrenReleaseHandle(WrenVM* vm, WrenHandle* handle);

View File

@ -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 &rarr;</a>
<a href="calling-c-from-wren.html">&larr; Calling C from Wren</a>

View File

@ -29,7 +29,6 @@
<li><a href="calling-c-from-wren.html">Calling C from Wren</a></li>
<li><a href="storing-c-data.html">Storing C Data</a></li>
<li><a href="configuring-the-vm.html">Configuring the VM</a></li>
<li><a href="application-lifecycle.html">Application Lifecycle</a></li>
</ul>
</section>
</nav>
@ -49,7 +48,6 @@
<li><a href="calling-c-from-wren.html">Calling C from Wren</a></li>
<li><a href="storing-c-data.html">Storing C Data</a></li>
<li><a href="configuring-the-vm.html">Configuring the VM</a></li>
<li><a href="application-lifecycle.html">Application Lifecycle</a></li>
</ul>
</td>
<td>