impress/vendor/gems/RocketAMF-1.0.0/ext/rocketamf_ext/class_mapping.c
Emi Matchu b1f06029f8 Moderize RocketAMF C types to fix build error
I'm not sure if this is a Mac-only problem or what, but we were getting incompatible-function-pointer errors when trying to build the RocketAMF C extensions. This fixes that! (Maybe it's like, Mac-only but as of Ruby 3.4 in specific? We're running RocketAMF in production on Ruby 3.4 right now without this. Shrug.)
2025-10-30 02:45:56 +00:00

487 lines
No EOL
15 KiB
C

#include <ruby.h>
#ifdef HAVE_RB_STR_ENCODE
#include <ruby/st.h>
#else
#include <st.h>
#endif
#include "utility.h"
extern VALUE mRocketAMF;
extern VALUE mRocketAMFExt;
VALUE cFastMappingSet;
VALUE cTypedHash;
ID id_use_ac;
ID id_use_ac_ivar;
ID id_mappings;
ID id_mappings_ivar;
ID id_hashset;
typedef struct {
VALUE mapset;
st_table* setter_cache;
st_table* prop_cache;
} CLASS_MAPPING;
typedef struct {
st_table* as_mappings;
st_table* rb_mappings;
} MAPSET;
/*
* Mark the as_mappings and rb_mappings hashes
*/
static void mapset_mark(MAPSET *set) {
if(!set) return;
rb_mark_tbl(set->as_mappings);
rb_mark_tbl(set->rb_mappings);
}
/*
* Free the mapping tables and struct
*/
int mapset_free_strtable_key(st_data_t key, st_data_t value, st_data_t ignored) {
xfree((void *)key);
return ST_DELETE;
}
static void mapset_free(MAPSET *set) {
st_foreach(set->as_mappings, mapset_free_strtable_key, 0);
st_free_table(set->as_mappings);
set->as_mappings = NULL;
st_foreach(set->rb_mappings, mapset_free_strtable_key, 0);
st_free_table(set->rb_mappings);
set->rb_mappings = NULL;
xfree(set);
}
/*
* Allocate mapset and populate mappings with built-in mappings
*/
static VALUE mapset_alloc(VALUE klass) {
MAPSET *set = ALLOC(MAPSET);
memset(set, 0, sizeof(MAPSET));
VALUE self = Data_Wrap_Struct(klass, mapset_mark, mapset_free, set);
// Initialize internal data
set->as_mappings = st_init_strtable();
set->rb_mappings = st_init_strtable();
return self;
}
/*
* call-seq:
* RocketAMF::Ext::MappingSet.new
*
* Creates a mapping set object and populates the default mappings
*/
static VALUE mapset_init(VALUE self) {
rb_funcall(self, rb_intern("map_defaults"), 0);
return self;
}
/*
* call-seq:
* m.map_defaults
*
* Adds required mapping configs, calling map for the required base mappings
*/
static VALUE mapset_map_defaults(VALUE self) {
const int NUM_MAPPINGS = 9;
const char* ruby_classes[] = {
"RocketAMF::Values::AbstractMessage",
"RocketAMF::Values::RemotingMessage",
"RocketAMF::Values::AsyncMessage",
"RocketAMF::Values::AsyncMessageExt",
"RocketAMF::Values::CommandMessage",
"RocketAMF::Values::CommandMessageExt",
"RocketAMF::Values::AcknowledgeMessage",
"RocketAMF::Values::AcknowledgeMessageExt",
"RocketAMF::Values::ErrorMessage"
};
const char* as_classes[] = {
"flex.messaging.messages.AbstractMessage",
"flex.messaging.messages.RemotingMessage",
"flex.messaging.messages.AsyncMessage",
"DSA",
"flex.messaging.messages.CommandMessage",
"DSC",
"flex.messaging.messages.AcknowledgeMessage",
"DSK",
"flex.messaging.messages.ErrorMessage"
};
int i;
ID map_id = rb_intern("map");
VALUE params = rb_hash_new();
VALUE as_sym = ID2SYM(rb_intern("as"));
VALUE ruby_sym = ID2SYM(rb_intern("ruby"));
for(i = 0; i < NUM_MAPPINGS; i++) {
rb_hash_aset(params, as_sym, rb_str_new2(as_classes[i]));
rb_hash_aset(params, ruby_sym, rb_str_new2(ruby_classes[i]));
rb_funcall(self, map_id, 1, params);
}
return self;
}
/*
* call-seq:
* m.map :as => 'com.example.Date', :ruby => "Example::Date'
*
* Map a given AS class to a ruby class. Use fully qualified names for both.
*/
static VALUE mapset_map(VALUE self, VALUE mapping) {
MAPSET *set;
Data_Get_Struct(self, MAPSET, set);
VALUE as_class = rb_hash_aref(mapping, ID2SYM(rb_intern("as")));
VALUE rb_class = rb_hash_aref(mapping, ID2SYM(rb_intern("ruby")));
st_insert(set->as_mappings, (st_data_t)strdup(RSTRING_PTR(as_class)), rb_class);
st_insert(set->rb_mappings, (st_data_t)strdup(RSTRING_PTR(rb_class)), as_class);
return Qnil;
}
/*
* Internal method for looking up a given ruby class's AS class name or Qnil if
* not found
*/
static VALUE mapset_as_lookup(VALUE self, const char* class_name) {
MAPSET *set;
Data_Get_Struct(self, MAPSET, set);
VALUE as_name;
if(st_lookup(set->rb_mappings, (st_data_t)class_name, &as_name)) {
return as_name;
} else {
return Qnil;
}
}
/*
* Internal method for looking up a given AS class names ruby class name mapping
* or Qnil if not found
*/
static VALUE mapset_rb_lookup(VALUE self, const char* class_name) {
MAPSET *set;
Data_Get_Struct(self, MAPSET, set);
VALUE rb_name;
if(st_lookup(set->as_mappings, (st_data_t)class_name, &rb_name)) {
return rb_name;
} else {
return Qnil;
}
}
/*
* Mark the mapset object and property lookup cache
*/
static void mapping_mark(CLASS_MAPPING *map) {
if(!map) return;
rb_gc_mark(map->mapset);
rb_mark_tbl(map->prop_cache);
}
/*
* Free prop cache table and struct
*/
static void mapping_free(CLASS_MAPPING *map) {
st_free_table(map->setter_cache);
st_free_table(map->prop_cache);
xfree(map);
}
/*
* Allocate class mapping struct
*/
static VALUE mapping_alloc(VALUE klass) {
CLASS_MAPPING *map = ALLOC(CLASS_MAPPING);
memset(map, 0, sizeof(CLASS_MAPPING));
VALUE self = Data_Wrap_Struct(klass, mapping_mark, mapping_free, map);
map->setter_cache = st_init_numtable();
map->prop_cache = st_init_numtable();
return self;
}
/*
* Class-level getter for use_array_collection
*/
static VALUE mapping_s_array_collection_get(VALUE klass) {
VALUE use_ac = rb_ivar_get(klass, id_use_ac_ivar);
if(use_ac == Qnil) {
use_ac = Qfalse;
rb_ivar_set(klass, id_use_ac_ivar, use_ac);
}
return use_ac;
}
/*
* Class-level setter for use_array_collection
*/
static VALUE mapping_s_array_collection_set(VALUE klass, VALUE use_ac) {
return rb_ivar_set(klass, id_use_ac_ivar, use_ac);
}
/*
* Return MappingSet for class mapper, creating if uninitialized
*/
static VALUE mapping_s_mappings(VALUE klass) {
VALUE mappings = rb_ivar_get(klass, id_mappings_ivar);
if(mappings == Qnil) {
mappings = rb_class_new_instance(0, NULL, cFastMappingSet);
rb_ivar_set(klass, id_mappings_ivar, mappings);
}
return mappings;
}
/*
* call-seq:
* mapper.define {|m| block } => nil
*
* Define class mappings in the block. Block is passed a MappingSet object as
* the first parameter. See RocketAMF::ClassMapping for details.
*/
static VALUE mapping_s_define(VALUE klass) {
if (rb_block_given_p()) {
VALUE mappings = rb_funcall(klass, id_mappings, 0);
rb_yield(mappings);
}
return Qnil;
}
/*
* Reset class mappings
*/
static VALUE mapping_s_reset(VALUE klass) {
rb_ivar_set(klass, id_use_ac_ivar, Qfalse);
rb_ivar_set(klass, id_mappings_ivar, Qnil);
return Qnil;
}
/*
* Initialize class mapping object, setting use_class_mapping to false
*/
static VALUE mapping_init(VALUE self) {
CLASS_MAPPING *map;
Data_Get_Struct(self, CLASS_MAPPING, map);
map->mapset = rb_funcall(CLASS_OF(self), id_mappings, 0);
VALUE use_ac = rb_funcall(CLASS_OF(self), id_use_ac, 0);
rb_ivar_set(self, id_use_ac_ivar, use_ac);
return self;
}
/*
* call-seq:
* mapper.get_as_class_name => str
*
* Returns the AS class name for the given ruby object. Will also take a string
* containing the ruby class name.
*/
static VALUE mapping_as_class_name(VALUE self, VALUE obj) {
CLASS_MAPPING *map;
Data_Get_Struct(self, CLASS_MAPPING, map);
int type = TYPE(obj);
const char* class_name;
if(type == T_STRING) {
// Use strings as the class name
class_name = RSTRING_PTR(obj);
} else {
// Look up the class name and use that
VALUE klass = CLASS_OF(obj);
class_name = rb_class2name(klass);
if(klass == cTypedHash) {
VALUE orig_name = rb_funcall(obj, rb_intern("type"), 0);
class_name = RSTRING_PTR(orig_name);
} else if(type == T_HASH) {
// Don't bother looking up hash mapping, but need to check class name first in case it's a typed hash
return Qnil;
}
}
return mapset_as_lookup(map->mapset, class_name);
}
/*
* call_seq:
* mapper.get_ruby_obj => obj
*
* Instantiates a ruby object using the mapping configuration based on the
* source AS class name. If there is no mapping defined, it returns a
* <tt>RocketAMF::Values::TypedHash</tt> with the serialized class name.
*/
static VALUE mapping_get_ruby_obj(VALUE self, VALUE name) {
CLASS_MAPPING *map;
Data_Get_Struct(self, CLASS_MAPPING, map);
VALUE argv[1];
VALUE ruby_class_name = mapset_rb_lookup(map->mapset, RSTRING_PTR(name));
if(ruby_class_name == Qnil) {
argv[0] = name;
return rb_class_new_instance(1, argv, cTypedHash);
} else {
VALUE base_const = rb_mKernel;
char* endptr;
char* ptr = RSTRING_PTR(ruby_class_name);
while((endptr = strstr(ptr,"::"))) {
endptr[0] = '\0'; // NULL terminate to make string ops work
base_const = rb_const_get(base_const, rb_intern(ptr));
endptr[0] = ':'; // Restore correct char
ptr = endptr + 2;
}
return rb_class_new_instance(0, NULL, rb_const_get(base_const, rb_intern(ptr)));
}
}
/*
* st_table iterator for populating a given object from a property hash
*/
static int mapping_populate_iter(st_data_t key_data, st_data_t val_data, st_data_t args_data) {
VALUE key = (VALUE)key_data;
VALUE val = (VALUE)val_data;
const VALUE *args = (const VALUE *)args_data;
CLASS_MAPPING *map;
Data_Get_Struct(args[0], CLASS_MAPPING, map);
VALUE obj = args[1];
if(TYPE(obj) == T_HASH) {
rb_hash_aset(obj, key, val);
return ST_CONTINUE;
}
if(TYPE(key) != T_SYMBOL) rb_raise(rb_eArgError, "Invalid type for property key: %d", TYPE(key));
// Calculate symbol for setter function
ID key_id = SYM2ID(key);
ID setter_id;
if(!st_lookup(map->setter_cache, key_id, &setter_id)) {
// Calculate symbol
const char* key_str = rb_id2name(key_id);
long len = strlen(key_str);
char* setter = ALLOC_N(char, len+2);
memcpy(setter, key_str, len);
setter[len] = '=';
setter[len+1] = '\0';
setter_id = rb_intern(setter);
xfree(setter);
// Store it
st_add_direct(map->setter_cache, key_id, setter_id);
}
if(rb_respond_to(obj, setter_id)) {
rb_funcall(obj, setter_id, 1, val);
} else if(rb_respond_to(obj, id_hashset)) {
rb_funcall(obj, id_hashset, 2, key, val);
}
return ST_CONTINUE;
}
/*
* call-seq:
* mapper.populate_ruby_obj(obj, props, dynamic_props=nil) => obj
*
* Populates the ruby object using the given properties. Property hashes MUST
* have symbol keys, or it will raise an exception.
*/
static VALUE mapping_populate(int argc, VALUE *argv, VALUE self) {
// Check args
VALUE obj, props, dynamic_props;
rb_scan_args(argc, argv, "21", &obj, &props, &dynamic_props);
VALUE args[2] = {self, obj};
st_foreach(RHASH_TBL(props), mapping_populate_iter, (st_data_t)args);
if(dynamic_props != Qnil) {
st_foreach(RHASH_TBL(dynamic_props), mapping_populate_iter, (st_data_t)args);
}
return obj;
}
/*
* call-seq:
* mapper.props_for_serialization(obj) => hash
*
* Extracts all exportable properties from the given ruby object and returns
* them in a hash. For performance purposes, property detection is only performed
* once for a given class instance, and then cached for all instances of that
* class. IF YOU'RE ADDING AND REMOVING PROPERTIES FROM CLASS INSTANCES YOU
* CANNOT USE THE FAST CLASS MAPPER.
*/
static VALUE mapping_props(VALUE self, VALUE obj) {
CLASS_MAPPING *map;
Data_Get_Struct(self, CLASS_MAPPING, map);
if(TYPE(obj) == T_HASH) {
return obj;
}
// Get "properties"
VALUE props_ary;
VALUE klass = CLASS_OF(obj);
long i, len;
if(!st_lookup(map->prop_cache, klass, &props_ary)) {
props_ary = rb_ary_new();
// Build props array
VALUE all_methods = rb_class_public_instance_methods(0, NULL, klass);
VALUE object_methods = rb_class_public_instance_methods(0, NULL, rb_cObject);
VALUE possible_methods = rb_funcall(all_methods, rb_intern("-"), 1, object_methods);
len = RARRAY_LEN(possible_methods);
for(i = 0; i < len; i++) {
VALUE meth = rb_obj_method(obj, RARRAY_PTR(possible_methods)[i]);
VALUE arity = rb_funcall(meth, rb_intern("arity"), 0);
if(FIX2INT(arity) == 0) {
rb_ary_push(props_ary, RARRAY_PTR(possible_methods)[i]);
}
}
// Store it
st_add_direct(map->prop_cache, klass, props_ary);
}
// Build properties hash using list of properties
VALUE props = rb_hash_new();
len = RARRAY_LEN(props_ary);
for(i = 0; i < len; i++) {
VALUE key = RARRAY_PTR(props_ary)[i];
ID getter = (TYPE(key) == T_STRING) ? rb_intern(RSTRING_PTR(key)) : SYM2ID(key);
rb_hash_aset(props, key, rb_funcall(obj, getter, 0));
}
return props;
}
void Init_rocket_amf_fast_class_mapping() {
// Define map set
cFastMappingSet = rb_define_class_under(mRocketAMFExt, "FastMappingSet", rb_cObject);
rb_define_alloc_func(cFastMappingSet, mapset_alloc);
rb_define_method(cFastMappingSet, "initialize", mapset_init, 0);
rb_define_method(cFastMappingSet, "map_defaults", mapset_map_defaults, 0);
rb_define_method(cFastMappingSet, "map", mapset_map, 1);
// Define FastClassMapping
VALUE cFastClassMapping = rb_define_class_under(mRocketAMFExt, "FastClassMapping", rb_cObject);
rb_define_alloc_func(cFastClassMapping, mapping_alloc);
rb_define_singleton_method(cFastClassMapping, "use_array_collection", mapping_s_array_collection_get, 0);
rb_define_singleton_method(cFastClassMapping, "use_array_collection=", mapping_s_array_collection_set, 1);
rb_define_singleton_method(cFastClassMapping, "mappings", mapping_s_mappings, 0);
rb_define_singleton_method(cFastClassMapping, "reset", mapping_s_reset, 0);
rb_define_singleton_method(cFastClassMapping, "define", mapping_s_define, 0);
rb_define_attr(cFastClassMapping, "use_array_collection", 1, 0);
rb_define_method(cFastClassMapping, "initialize", mapping_init, 0);
rb_define_method(cFastClassMapping, "get_as_class_name", mapping_as_class_name, 1);
rb_define_method(cFastClassMapping, "get_ruby_obj", mapping_get_ruby_obj, 1);
rb_define_method(cFastClassMapping, "populate_ruby_obj", mapping_populate, -1);
rb_define_method(cFastClassMapping, "props_for_serialization", mapping_props, 1);
// Cache values
cTypedHash = rb_const_get(rb_const_get(mRocketAMF, rb_intern("Values")), rb_intern("TypedHash"));
id_use_ac = rb_intern("use_array_collection");
id_use_ac_ivar = rb_intern("@use_array_collection");
id_mappings = rb_intern("mappings");
id_mappings_ivar = rb_intern("@mappings");
id_hashset = rb_intern("[]=");
}